feat: monorepo with NestJS API, admin panel and DB-driven website
Restructure the repo into a pnpm + Turborepo monorepo: - apps/web: Nuxt 4 site, now server-rendered and reading all content from the API; pages cached with SWR and purged on content changes - apps/api: NestJS 12 + Prisma 7 + PostgreSQL. Auth with short-lived access tokens and rotating httpOnly refresh cookies, roles (ADMIN, EDITOR), catalog, editable site sections, media upload to S3-compatible storage (converted to WebP), contact messages, audit log - apps/admin: Vue 3 admin panel (Ant Design Vue, Pinia, Tailwind) for products, categories, site sections, messages, media and users - packages/shared: Zod schemas and types shared by API and admin Also: - seed script that loads the original 8 categories / 89 products and section content - Docker images for api (with a one-off migrate service), web, admin, and an nginx gateway for both domains - dev and production compose files, daily database backup service - API e2e tests Removes the committed static out/ build and the old root Dockerfile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
31076174e2
commit
789d224ea0
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import type { IconName } from "@imen/shared";
|
||||
import {
|
||||
Award,
|
||||
BadgeCheck,
|
||||
Building2,
|
||||
ClipboardCheck,
|
||||
Clock,
|
||||
Construction,
|
||||
Ear,
|
||||
Factory,
|
||||
FileCheck,
|
||||
FileSearch,
|
||||
Flame,
|
||||
Footprints,
|
||||
Fuel,
|
||||
Gauge,
|
||||
Glasses,
|
||||
Globe,
|
||||
GraduationCap,
|
||||
Hand,
|
||||
Handshake,
|
||||
HardHat,
|
||||
Headset,
|
||||
HeartPulse,
|
||||
Leaf,
|
||||
Lock,
|
||||
Mail,
|
||||
MapPin,
|
||||
Medal,
|
||||
Microscope,
|
||||
Package,
|
||||
PackageCheck,
|
||||
Phone,
|
||||
Pickaxe,
|
||||
ShieldAlert,
|
||||
ShieldCheck,
|
||||
ShieldPlus,
|
||||
Shirt,
|
||||
Siren,
|
||||
Star,
|
||||
Target,
|
||||
ThumbsUp,
|
||||
TrafficCone,
|
||||
TriangleAlert,
|
||||
Truck,
|
||||
Users,
|
||||
Warehouse,
|
||||
Wind,
|
||||
Wrench,
|
||||
Zap,
|
||||
} from "lucide-vue-next";
|
||||
import type { Component } from "vue";
|
||||
|
||||
/** Explicit registry (matches ICON_NAMES in @imen/shared) so only these icons are bundled */
|
||||
const ICONS: Record<IconName, Component> = {
|
||||
ShieldCheck,
|
||||
ShieldPlus,
|
||||
ShieldAlert,
|
||||
BadgeCheck,
|
||||
Award,
|
||||
Medal,
|
||||
PackageCheck,
|
||||
Package,
|
||||
Truck,
|
||||
Warehouse,
|
||||
Globe,
|
||||
Users,
|
||||
Handshake,
|
||||
Headset,
|
||||
ClipboardCheck,
|
||||
FileCheck,
|
||||
FileSearch,
|
||||
GraduationCap,
|
||||
Gauge,
|
||||
Microscope,
|
||||
Wrench,
|
||||
HardHat,
|
||||
Shirt,
|
||||
Footprints,
|
||||
Hand,
|
||||
Glasses,
|
||||
Ear,
|
||||
Wind,
|
||||
TriangleAlert,
|
||||
Siren,
|
||||
TrafficCone,
|
||||
Construction,
|
||||
Factory,
|
||||
Building2,
|
||||
Fuel,
|
||||
Pickaxe,
|
||||
Flame,
|
||||
Zap,
|
||||
Leaf,
|
||||
HeartPulse,
|
||||
Clock,
|
||||
Phone,
|
||||
Mail,
|
||||
MapPin,
|
||||
Star,
|
||||
ThumbsUp,
|
||||
Target,
|
||||
Lock,
|
||||
};
|
||||
|
||||
defineProps<{ name: string }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="ICONS[name as IconName] ?? ShieldCheck" />
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { ImageOff } from "lucide-vue-next";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
aspectClass?: string;
|
||||
}>(),
|
||||
{
|
||||
label: "تصویر محصول",
|
||||
aspectClass: "aspect-[4/3]",
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="`flex flex-col items-center justify-center border border-dashed border-line bg-panel ${aspectClass}`"
|
||||
>
|
||||
<ImageOff class="size-10 text-muted/30" />
|
||||
<span class="mt-2 font-mono text-[10px] tracking-wider text-muted/40">{{ label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
/** Renders editor text where "\n" means a line break */
|
||||
const props = defineProps<{ text: string }>();
|
||||
const lines = computed(() => props.text.split("\n"));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-for="(line, i) in lines" :key="i"><br v-if="i > 0" />{{ line }}</template>
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink to="/" class="flex items-center gap-3" aria-label="ایمن پیشگامان">
|
||||
<span
|
||||
class="flex size-10 items-center justify-center rounded-xl bg-white shadow-[0_0_28px_rgba(10,30,92,0.45)]"
|
||||
>
|
||||
<img
|
||||
src="/images/logo.png"
|
||||
alt="ایمن پیشگامان"
|
||||
width="28"
|
||||
height="28"
|
||||
class="size-7 object-contain"
|
||||
/>
|
||||
</span>
|
||||
<span class="leading-tight">
|
||||
<span class="block text-lg font-black">ایمن پیشگامان</span>
|
||||
<span class="block font-mono text-[9px] tracking-[0.32em] text-muted">
|
||||
IMEN PISHGAMAN CO.
|
||||
</span>
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { motion } from "motion-v";
|
||||
|
||||
const props = withDefaults(defineProps<{ delay?: number }>(), { delay: 0 });
|
||||
const motionProps = computed(() => fade(props.delay, 34));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<motion.div v-bind="motionProps">
|
||||
<slot />
|
||||
</motion.div>
|
||||
</template>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import Lines from "./Lines.vue";
|
||||
import Reveal from "./Reveal.vue";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
tag: string;
|
||||
/** title text; "\n" = line break. Use the #title slot for custom markup */
|
||||
title?: string;
|
||||
/** optional accented last line of the title */
|
||||
highlight?: string;
|
||||
desc?: string;
|
||||
center?: boolean;
|
||||
headingId?: string;
|
||||
}>(),
|
||||
{ center: false },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="center ? 'text-center' : ''">
|
||||
<Reveal>
|
||||
<div :class="`flex items-center gap-3 ${center ? 'justify-center' : ''}`">
|
||||
<span class="font-mono text-sm text-accent">//</span>
|
||||
<span class="font-mono text-xs tracking-[0.25em] text-muted">
|
||||
{{ tag }}
|
||||
</span>
|
||||
<span class="h-px w-14 bg-line" />
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal :delay="0.08">
|
||||
<h2
|
||||
:id="headingId"
|
||||
class="mt-5 text-3xl font-black leading-[1.25] md:text-5xl md:leading-[1.2]"
|
||||
>
|
||||
<slot name="title">
|
||||
<Lines v-if="title" :text="title" />
|
||||
<template v-if="highlight">
|
||||
<br />
|
||||
<span class="text-accent">{{ highlight }}</span>
|
||||
</template>
|
||||
</slot>
|
||||
</h2>
|
||||
</Reveal>
|
||||
<Reveal v-if="desc" :delay="0.16">
|
||||
<p :class="`mt-5 max-w-xl leading-8 text-muted ${center ? 'mx-auto' : ''}`">
|
||||
{{ desc }}
|
||||
</p>
|
||||
</Reveal>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user