Files
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

82 lines
3.1 KiB
Vue

<script setup lang="ts">
import DynamicIcon from "~/components/ui/DynamicIcon.vue";
import Lines from "~/components/ui/Lines.vue";
import Reveal from "~/components/ui/Reveal.vue";
import SectionHead from "~/components/ui/SectionHead.vue";
const about = computed(() => useSiteStore().sections.about);
</script>
<template>
<section
id="about"
aria-labelledby="about-title"
class="relative overflow-hidden border-t border-line py-24 md:py-32"
>
<div class="absolute inset-0 bg-grid-fine opacity-50" />
<div class="relative mx-auto max-w-[1480px] px-5 md:px-8">
<div class="grid items-center gap-14 lg:grid-cols-2">
<!-- ---- image ---- -->
<Reveal class="relative order-2 lg:order-1" :delay="0.1">
<div class="relative overflow-hidden rounded-2xl border border-line">
<img
:src="about.image.url"
:alt="about.image.alt"
width="800"
height="600"
loading="lazy"
decoding="async"
class="aspect-[4/3] w-full object-cover"
/>
<div class="absolute inset-0 bg-gradient-to-t from-bg/80 via-transparent to-transparent" />
<div
class="absolute bottom-5 start-5 flex items-center gap-4 rounded-xl border border-line bg-bg/80 px-5 py-4 backdrop-blur-md"
>
<span class="font-mono text-3xl font-bold text-accent">{{ about.badge.value }}</span>
<span class="text-xs leading-5 text-muted">
<Lines :text="about.badge.label" />
</span>
</div>
</div>
<div class="hazard absolute -top-3 -end-3 h-6 w-28 rounded-full opacity-90" />
</Reveal>
<!-- ---- text ---- -->
<div class="order-1 lg:order-2">
<SectionHead heading-id="about-title" v-bind="about.head" />
<Reveal :delay="0.2">
<p class="mt-6 leading-9 text-muted">
{{ about.body }}
</p>
</Reveal>
<div class="mt-10 space-y-1">
<Reveal v-for="(t, i) in about.timeline" :key="t.year" :delay="0.25 + i * 0.07">
<div
class="group border-s-2 border-accent/30 ps-6 transition-colors duration-300 hover:border-accent"
>
<span class="font-mono text-sm font-bold text-accent">{{ t.year }}</span>
<p class="mt-1 pb-5 text-sm leading-7 text-ink/75">{{ t.text }}</p>
</div>
</Reveal>
</div>
<Reveal :delay="0.5">
<div class="mt-4 flex flex-wrap gap-3">
<span
v-for="f in about.features"
:key="f.label"
class="inline-flex items-center gap-2 rounded-full border border-line bg-panel/60 px-4 py-2 text-xs text-ink/80 transition-colors hover:border-accent/50 hover:text-ink"
>
<DynamicIcon :name="f.icon" class="size-4 text-accent" />
{{ f.label }}
</span>
</div>
</Reveal>
</div>
</div>
</div>
</section>
</template>