Files
imen_pishghaman/apps/web/app/components/sections/Services.vue
T
S. EsmaeiliandClaude Opus 5.5 789d224ea0 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>
2026-10-21 20:31:05 +03:30

94 lines
3.6 KiB
Vue

<script setup lang="ts">
import { AnimatePresence, motion } from "motion-v";
import { Plus } from "lucide-vue-next";
import DynamicIcon from "~/components/ui/DynamicIcon.vue";
import Reveal from "~/components/ui/Reveal.vue";
import SectionHead from "~/components/ui/SectionHead.vue";
import { serviceSchema } from "#shared/seo/schema";
const services = computed(() => useSiteStore().sections.services);
const seo = useSeoContext();
useJsonLd(
services.value.items.map((s) => serviceSchema(seo, { name: s.title, description: s.desc })),
"services",
);
const open = ref(0);
</script>
<template>
<section id="services" aria-labelledby="services-title" class="border-t border-line py-24 md:py-32">
<div class="mx-auto max-w-[1480px] px-5 md:px-8">
<SectionHead heading-id="services-title" v-bind="services.head" />
<div class="mt-16">
<Reveal v-for="(s, i) in services.items" :key="s.title" :delay="i * 0.05">
<div :class="`border-t border-line ${i === services.items.length - 1 ? 'border-b' : ''}`">
<button
class="group flex w-full items-center gap-5 py-7 text-start md:gap-10 md:py-9"
@click="open = open === i ? -1 : i"
>
<span
:class="`font-mono text-sm transition-colors duration-300 md:text-base ${
open === i ? 'text-accent' : 'text-muted'
}`"
>
{{ faNum(String(i + 1).padStart(2, "0")) }}
</span>
<span
:class="`flex-1 text-2xl font-black transition-colors duration-300 md:text-4xl ${
open === i ? 'text-accent' : 'text-ink group-hover:text-accent'
}`"
>
{{ s.title }}
</span>
<span
:class="`hidden size-12 shrink-0 items-center justify-center rounded-full border transition-all duration-500 md:flex ${
open === i
? 'rotate-45 border-accent bg-accent text-bg'
: 'border-line text-muted group-hover:border-accent/50 group-hover:text-accent'
}`"
>
<Plus class="size-5" />
</span>
</button>
<AnimatePresence :initial="false">
<motion.div
v-if="open === i"
:initial="{ height: 0, opacity: 0 }"
:animate="{ height: 'auto', opacity: 1 }"
:exit="{ height: 0, opacity: 0 }"
:transition="{ duration: 0.55, ease: EASE }"
class="overflow-hidden"
>
<div class="grid gap-8 pb-10 md:grid-cols-2 md:ps-16">
<div class="flex items-start gap-4">
<span
class="flex size-12 shrink-0 items-center justify-center rounded-xl bg-accent/10 text-accent"
>
<DynamicIcon :name="s.icon" class="size-6" />
</span>
<p class="text-sm leading-8 text-muted">{{ s.desc }}</p>
</div>
<ul class="space-y-3.5">
<li
v-for="it in s.items"
:key="it"
class="flex items-center gap-3 text-sm text-ink/80"
>
<span class="size-1.5 shrink-0 rounded-full bg-accent" />
{{ it }}
</li>
</ul>
</div>
</motion.div>
</AnimatePresence>
</div>
</Reveal>
</div>
</div>
</section>
</template>