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>
80 lines
4.4 KiB
Markdown
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, className→class -->
|
|
<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).
|