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>
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 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 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, 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,Logofrom@/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 groupedInputset 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).