gigafibre-fsm/apps/website-vue/PORTING_GUIDE.md
louispaulb 3c7f04870c portal: Vue 3 + Tailwind + shadcn-vue rewrite of www.gigafibre.ca (preview at /next)
Pixel-identical port of the React marketing site: Tailwind config, HSL theme
tokens, fonts (Plus Jakarta Sans / Space Grotesk) and custom utilities copied
verbatim; shadcn-vue (reka-ui) components; all 33 routes ported. Served at
www.gigafibre.ca/next (noindex) behind nginx, ahead of replacing the React site.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 21:52:14 -04:00

80 lines
4.4 KiB
Markdown

# React → Vue port conventions (gigafibre website)
We are porting the React site at `../website` (React 18 + Vite + Tailwind 3 + shadcn/ui + Radix)
to this Vue 3 app (`../website-vue`, Vue 3 `<script setup>` + Vite + Tailwind 3 + shadcn-vue + reka-ui).
**Goal: pixel-identical output.** The Tailwind config, CSS tokens, fonts and custom utilities are already
copied verbatim, so **keep the exact same Tailwind class strings** on every element.
## The golden rule
Copy the JSX markup and its `className` strings **unchanged** into the Vue `<template>` (rename `className`→`class`).
The visual result comes from those classes; do not "improve" or restructure them.
## Mechanical mappings
| React | Vue |
|---|---|
| `className="..."` | `class="..."` |
| `className={cn(a, b)}` | `:class="cn(a, b)"` (import `cn` from `@/lib/utils`) |
| `{expr}` in JSX text | `{{ expr }}` |
| `{items.map(x => <Card .../>)}` | `<Card v-for="x in items" :key="x.id" ... />` |
| `{cond && <X/>}` | `<X v-if="cond" />` |
| `{cond ? <A/> : <B/>}` | `<A v-if="cond" /> <B v-else />` |
| `onClick={fn}` | `@click="fn"` |
| `onChange={e => setX(e.target.value)}` | `v-model="x"` on inputs |
| `useState(v)` | `const x = ref(v)` (`.value` in script, bare in template) |
| `useEffect(fn, [])` | `onMounted(fn)` |
| `useEffect(fn, [dep])` | `watch(() => dep, fn)` |
| `<Link to="/x">` (react-router) | `<RouterLink to="/x">` |
| `useNavigate()``navigate("/x")` | `const router = useRouter(); router.push("/x")` |
| `useLocation()` | `const route = useRoute()` |
| lucide-react `import { Wifi } from "lucide-react"` | `import { Wifi } from "lucide-vue-next"` (same icon names) |
| `<Icon className="h-5 w-5" />` | `<Icon class="h-5 w-5" />` |
| framer-motion `<motion.div initial=... animate=... />` | `<div v-motion :initial="..." :enter="...">` (from `@vueuse/motion`); `whileInView``:visible-once="..."` |
| `AnimatePresence` | usually a `<Transition>` or just `v-if` — keep it simple, match the visual |
| Toasts: `useToast()` / `toast(...)` (shadcn) | `import { useToast } from "@/components/ui/toast/use-toast"` (same API) |
| `sonner` `toast()` | `import { toast } from "vue-sonner"` |
| react-hook-form + zod | `vee-validate` + `zod` via `@/components/ui/form`, OR for simple cases just `ref`s + manual zod `.safeParse` |
## shadcn-vue components
Import from the folder index, e.g. `import { Button } from "@/components/ui/button"`,
`import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"`.
Component/prop names and the `variant`/`size` props match shadcn/ui. Slots replace React children.
`asChild` exists in reka-ui too. Available under `src/components/ui/`: accordion, alert, alert-dialog,
aspect-ratio, avatar, badge, breadcrumb, button, calendar, card, carousel, checkbox, collapsible, command,
context-menu, dialog, drawer, dropdown-menu, form, hover-card, input, label, menubar, navigation-menu,
pagination, popover, progress, radio-group, resizable, scroll-area, select, separator, sheet, skeleton,
slider, sonner, switch, table, tabs, textarea, toast, toggle, toggle-group, tooltip.
## Structure of a ported page
```vue
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { RouterLink } from "vue-router";
import { Button } from "@/components/ui/button";
import { Wifi } from "lucide-vue-next";
import Header from "@/components/layout/Header.vue";
import Footer from "@/components/layout/Footer.vue";
// ...page-local data/state
</script>
<template>
<div class="min-h-screen bg-background">
<Header />
<!-- exact JSX markup, classNameclass -->
<Footer />
</div>
</template>
```
## Do / Don't
- DO keep every Tailwind class string identical to the React source.
- DO keep the same DOM structure, headings, copy (French text) and image URLs.
- DO reuse `Header`, `Footer`, `BusinessPageLayout`, `GlassCard`, `Logo` from `@/components/...`.
- DON'T add new colors, spacing, or restyle. DON'T invent content.
- DON'T pull in new deps; everything needed is installed.
- If a React file uses `input-otp` (not in the shadcn-vue registry), use a plain grouped `Input` set or note it.
- If something can't be perfectly matched, leave a `<!-- TODO(port): ... -->` and keep the closest visual.
## Assets
Static files live in `public/` and resolve under `/next/...` (Vite `base: '/next/'`). Copy any images the
React app references from `../website/public` into `./public` (same relative paths).