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

4.4 KiB

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 classNameclass). 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 refs + 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

<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).