feat: luxury birthday landing page (Vite+React+TS+Tailwind+Framer Motion)
This commit is contained in:
commit
243c2fd803
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
19
index.html
Normal file
19
index.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>С Днём Рождения — Luxury Birthday Experience</title>
|
||||||
|
<meta name="description" content="Премиальное digital-поздравление с днём рождения." />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;500;600&family=Playfair+Display:wght@300;400;500;600&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2736
package-lock.json
generated
Normal file
2736
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "birthday-luxury",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"framer-motion": "^11.11.17",
|
||||||
|
"lucide-react": "^0.460.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"tailwindcss": "^3.4.15",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"vite": "^5.4.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
315
src/App.tsx
Normal file
315
src/App.tsx
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { motion, AnimatePresence, useScroll, useTransform } from "framer-motion";
|
||||||
|
import { Play, RotateCcw, Share2, Sparkles } from "lucide-react";
|
||||||
|
|
||||||
|
const copy = {
|
||||||
|
intro: ["Еще один год.", "Еще один уровень.", "Еще одна глава."],
|
||||||
|
sectionTwoTitle: "Время — главный показатель ценности.",
|
||||||
|
sectionTwoSubtitle: "И сегодня его стало больше.",
|
||||||
|
hero: "С ДНЕМ РОЖДЕНИЯ",
|
||||||
|
heroSubtitle: "Пусть следующий этап будет громче, сильнее и красивее предыдущего.",
|
||||||
|
};
|
||||||
|
|
||||||
|
function Grain() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="pointer-events-none fixed inset-0 z-50 opacity-[0.075] mix-blend-soft-light"
|
||||||
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
"url('data:image/svg+xml,%3Csvg viewBox=%220 0 260 260%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22noiseFilter%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%220.7%22 numOctaves=%223%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23noiseFilter)%22/%3E%3C/svg%3E')",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LuxuryParticles() {
|
||||||
|
const particles = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.from({ length: 72 }, (_, i) => ({
|
||||||
|
id: i,
|
||||||
|
left: `${Math.random() * 100}%`,
|
||||||
|
top: `${Math.random() * 100}%`,
|
||||||
|
size: Math.random() * 3.8 + 1.2,
|
||||||
|
delay: Math.random() * 8,
|
||||||
|
duration: Math.random() * 9 + 8,
|
||||||
|
opacity: Math.random() * 0.38 + 0.12,
|
||||||
|
})),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
||||||
|
{particles.map((p) => (
|
||||||
|
<motion.span
|
||||||
|
key={p.id}
|
||||||
|
className="absolute rounded-full bg-[#d8b76d] shadow-[0_0_22px_rgba(216,183,109,0.55)]"
|
||||||
|
style={{ left: p.left, top: p.top, width: p.size, height: p.size, opacity: p.opacity }}
|
||||||
|
animate={{ y: [-20, -120, -20], x: [0, 18, -8, 0], scale: [1, 1.45, 0.9, 1] }}
|
||||||
|
transition={{ duration: p.duration, repeat: Infinity, delay: p.delay, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AmbientLights({ mouse }: { mouse: { x: number; y: number } }) {
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none fixed inset-0 overflow-hidden bg-[#020202]">
|
||||||
|
<motion.div
|
||||||
|
className="absolute -top-48 left-1/2 h-[38rem] w-[38rem] -translate-x-1/2 rounded-full bg-[#b99445]/20 blur-[115px]"
|
||||||
|
animate={{ scale: [1, 1.14, 1], opacity: [0.45, 0.74, 0.45] }}
|
||||||
|
transition={{ duration: 9, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="absolute bottom-[-18rem] left-[-12rem] h-[40rem] w-[40rem] rounded-full bg-[#87621e]/20 blur-[130px]"
|
||||||
|
animate={{ x: mouse.x * 18, y: mouse.y * 12 }}
|
||||||
|
transition={{ type: "spring", stiffness: 34, damping: 20 }}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="absolute right-[-15rem] top-[20%] h-[34rem] w-[34rem] rounded-full bg-white/10 blur-[140px]"
|
||||||
|
animate={{ x: mouse.x * -14, y: mouse.y * -10 }}
|
||||||
|
transition={{ type: "spring", stiffness: 34, damping: 20 }}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_10%,rgba(255,231,171,0.12),transparent_28%),linear-gradient(180deg,rgba(0,0,0,0.05),rgba(0,0,0,0.94))]" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PremiumButton({
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
icon: Icon,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
icon?: React.ElementType;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className="group inline-flex h-12 items-center rounded-full border border-white/15 bg-white/[0.055] px-6 text-sm font-light tracking-[0.22em] text-white/85 shadow-[0_12px_50px_rgba(0,0,0,0.35)] backdrop-blur-xl transition-all duration-500 hover:border-[#d8b76d]/60 hover:bg-[#d8b76d]/10 hover:text-white"
|
||||||
|
>
|
||||||
|
{Icon && <Icon className="mr-2 h-4 w-4 transition-transform duration-500 group-hover:scale-110" />}
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LuxuryBirthdayLanding() {
|
||||||
|
const [started, setStarted] = useState(false);
|
||||||
|
const [mouse, setMouse] = useState({ x: 0, y: 0 });
|
||||||
|
const pageRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const { scrollYProgress } = useScroll({ target: pageRef, offset: ["start start", "end end"] });
|
||||||
|
const scale = useTransform(scrollYProgress, [0.35, 0.68], [0.86, 1.08]);
|
||||||
|
const glow = useTransform(scrollYProgress, [0.5, 0.7], [0.2, 1]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onMove = (e: MouseEvent) => {
|
||||||
|
setMouse({ x: e.clientX / window.innerWidth - 0.5, y: e.clientY / window.innerHeight - 0.5 });
|
||||||
|
};
|
||||||
|
window.addEventListener("mousemove", onMove);
|
||||||
|
return () => window.removeEventListener("mousemove", onMove);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scrollToNext = () => {
|
||||||
|
setStarted(true);
|
||||||
|
setTimeout(() => document.getElementById("time")?.scrollIntoView({ behavior: "smooth" }), 450);
|
||||||
|
};
|
||||||
|
|
||||||
|
const replay = () => {
|
||||||
|
setStarted(false);
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const share = async () => {
|
||||||
|
const data = {
|
||||||
|
title: "С Днем Рождения",
|
||||||
|
text: "Открой премиальное digital-поздравление.",
|
||||||
|
url: window.location.href,
|
||||||
|
};
|
||||||
|
if (navigator.share) await navigator.share(data);
|
||||||
|
else await navigator.clipboard.writeText(window.location.href);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main
|
||||||
|
ref={pageRef}
|
||||||
|
className="relative min-h-screen overflow-x-hidden bg-black text-white selection:bg-[#d8b76d]/30"
|
||||||
|
>
|
||||||
|
<AmbientLights mouse={mouse} />
|
||||||
|
<Grain />
|
||||||
|
|
||||||
|
<section className="relative flex min-h-screen items-center justify-center px-5 py-12">
|
||||||
|
<LuxuryParticles />
|
||||||
|
<div className="absolute inset-x-5 top-6 z-10 mx-auto flex max-w-6xl items-center justify-between rounded-full border border-white/10 bg-white/[0.035] px-5 py-3 backdrop-blur-2xl">
|
||||||
|
<div className="flex items-center gap-2 text-[10px] uppercase tracking-[0.34em] text-white/55">
|
||||||
|
<Sparkles className="h-3.5 w-3.5 text-[#d8b76d]" />
|
||||||
|
Luxury Birthday Experience
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative z-10 mx-auto max-w-5xl text-center">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{!started ? (
|
||||||
|
<motion.div
|
||||||
|
key="intro"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0, filter: "blur(18px)", scale: 0.98 }}
|
||||||
|
transition={{ duration: 1.2 }}
|
||||||
|
>
|
||||||
|
<div className="mb-8 text-[11px] uppercase tracking-[0.42em] text-[#d8b76d]/80">
|
||||||
|
Private celebration signal
|
||||||
|
</div>
|
||||||
|
<div className="space-y-5 md:space-y-8">
|
||||||
|
{copy.intro.map((line, i) => (
|
||||||
|
<motion.h1
|
||||||
|
key={line}
|
||||||
|
className="font-serif text-5xl font-light leading-[0.95] tracking-[-0.06em] text-white md:text-8xl lg:text-9xl"
|
||||||
|
initial={{ opacity: 0, y: 30, filter: "blur(18px)" }}
|
||||||
|
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
||||||
|
transition={{ duration: 1.35, delay: 0.35 + i * 0.58, ease: [0.16, 1, 0.3, 1] }}
|
||||||
|
>
|
||||||
|
{line}
|
||||||
|
</motion.h1>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 18 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 2.55, duration: 0.9 }}
|
||||||
|
className="mt-12"
|
||||||
|
>
|
||||||
|
<PremiumButton onClick={scrollToNext} icon={Play}>
|
||||||
|
Продолжить
|
||||||
|
</PremiumButton>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
key="started"
|
||||||
|
initial={{ opacity: 0, scale: 0.96, filter: "blur(12px)" }}
|
||||||
|
animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
|
<p className="text-sm uppercase tracking-[0.5em] text-[#d8b76d]/80">Experience started</p>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="time" className="relative flex min-h-screen items-center justify-center px-5 py-28">
|
||||||
|
<LuxuryParticles />
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-x-8 top-1/2 h-px bg-gradient-to-r from-transparent via-[#d8b76d]/45 to-transparent"
|
||||||
|
style={{ opacity: glow }}
|
||||||
|
/>
|
||||||
|
<div className="relative z-10 mx-auto max-w-5xl text-center">
|
||||||
|
<motion.p
|
||||||
|
className="mb-8 text-[11px] uppercase tracking-[0.42em] text-white/45"
|
||||||
|
initial={{ opacity: 0, y: 24 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: false, amount: 0.5 }}
|
||||||
|
transition={{ duration: 1 }}
|
||||||
|
>
|
||||||
|
Limited edition moment
|
||||||
|
</motion.p>
|
||||||
|
<motion.h2
|
||||||
|
className="font-serif text-5xl font-light leading-[1.02] tracking-[-0.055em] md:text-8xl"
|
||||||
|
initial={{ opacity: 0, y: 38, filter: "blur(16px)" }}
|
||||||
|
whileInView={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
||||||
|
viewport={{ once: false, amount: 0.45 }}
|
||||||
|
transition={{ duration: 1.35, ease: [0.16, 1, 0.3, 1] }}
|
||||||
|
>
|
||||||
|
{copy.sectionTwoTitle}
|
||||||
|
</motion.h2>
|
||||||
|
<motion.p
|
||||||
|
className="mx-auto mt-8 max-w-2xl text-xl font-light leading-relaxed text-white/60 md:text-2xl"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: false, amount: 0.45 }}
|
||||||
|
transition={{ duration: 1.1, delay: 0.24 }}
|
||||||
|
>
|
||||||
|
{copy.sectionTwoSubtitle}
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="relative flex min-h-screen items-center justify-center overflow-hidden px-5 py-28">
|
||||||
|
<LuxuryParticles />
|
||||||
|
<motion.div
|
||||||
|
style={{ scale }}
|
||||||
|
className="absolute h-[34rem] w-[34rem] rounded-full border border-[#d8b76d]/20 bg-[#d8b76d]/[0.045] shadow-[0_0_160px_rgba(216,183,109,0.32)] blur-[1px]"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="absolute h-[1px] w-[90vw] bg-gradient-to-r from-transparent via-white/70 to-transparent"
|
||||||
|
initial={{ scaleX: 0, opacity: 0 }}
|
||||||
|
whileInView={{ scaleX: 1, opacity: [0, 1, 0.25] }}
|
||||||
|
viewport={{ once: false, amount: 0.5 }}
|
||||||
|
transition={{ duration: 1.7, ease: "easeOut" }}
|
||||||
|
/>
|
||||||
|
<div className="relative z-10 mx-auto max-w-6xl text-center">
|
||||||
|
<motion.div
|
||||||
|
className="mx-auto mb-8 h-20 w-20 rounded-full border border-[#d8b76d]/35 bg-white/[0.04] p-5 shadow-[0_0_90px_rgba(216,183,109,0.35)] backdrop-blur-xl"
|
||||||
|
initial={{ opacity: 0, scale: 0.4, rotate: -20 }}
|
||||||
|
whileInView={{ opacity: 1, scale: 1, rotate: 0 }}
|
||||||
|
viewport={{ once: false, amount: 0.55 }}
|
||||||
|
transition={{ duration: 1.05, ease: [0.16, 1, 0.3, 1] }}
|
||||||
|
>
|
||||||
|
<Sparkles className="h-full w-full text-[#d8b76d]" />
|
||||||
|
</motion.div>
|
||||||
|
<motion.h2
|
||||||
|
className="bg-gradient-to-b from-white via-white to-[#c8a65e] bg-clip-text font-serif text-6xl font-light leading-[0.9] tracking-[-0.075em] text-transparent md:text-9xl lg:text-[10rem]"
|
||||||
|
initial={{ opacity: 0, scale: 0.86, filter: "blur(22px)" }}
|
||||||
|
whileInView={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
|
||||||
|
viewport={{ once: false, amount: 0.42 }}
|
||||||
|
transition={{ duration: 1.55, ease: [0.16, 1, 0.3, 1] }}
|
||||||
|
>
|
||||||
|
{copy.hero}
|
||||||
|
</motion.h2>
|
||||||
|
<motion.p
|
||||||
|
className="mx-auto mt-9 max-w-3xl text-lg font-light leading-relaxed text-white/70 md:text-2xl"
|
||||||
|
initial={{ opacity: 0, y: 24 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: false, amount: 0.5 }}
|
||||||
|
transition={{ duration: 1.2, delay: 0.26 }}
|
||||||
|
>
|
||||||
|
{copy.heroSubtitle}
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="relative flex min-h-screen items-center justify-center px-5 py-28">
|
||||||
|
<div className="absolute inset-x-5 mx-auto max-w-4xl rounded-[3rem] border border-white/10 bg-white/[0.035] p-8 shadow-[0_30px_120px_rgba(0,0,0,0.48)] backdrop-blur-2xl md:p-14" />
|
||||||
|
<div className="relative z-10 mx-auto max-w-3xl text-center">
|
||||||
|
<motion.p
|
||||||
|
className="text-[11px] uppercase tracking-[0.48em] text-[#d8b76d]/80"
|
||||||
|
initial={{ opacity: 0, y: 18 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: false, amount: 0.6 }}
|
||||||
|
transition={{ duration: 0.9 }}
|
||||||
|
>
|
||||||
|
The next chapter begins now
|
||||||
|
</motion.p>
|
||||||
|
<motion.h2
|
||||||
|
className="mt-8 font-serif text-4xl font-light leading-tight tracking-[-0.04em] text-white md:text-7xl"
|
||||||
|
initial={{ opacity: 0, y: 26, filter: "blur(14px)" }}
|
||||||
|
whileInView={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
||||||
|
viewport={{ once: false, amount: 0.5 }}
|
||||||
|
transition={{ duration: 1.25, ease: [0.16, 1, 0.3, 1] }}
|
||||||
|
>
|
||||||
|
Пусть этот день звучит как начало чего-то большого.
|
||||||
|
</motion.h2>
|
||||||
|
<div className="mt-12 flex flex-col items-center justify-center gap-4 sm:flex-row">
|
||||||
|
<PremiumButton onClick={replay} icon={RotateCcw}>
|
||||||
|
Повторить
|
||||||
|
</PremiumButton>
|
||||||
|
<PremiumButton onClick={share} icon={Share2}>
|
||||||
|
Поделиться
|
||||||
|
</PremiumButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
src/index.css
Normal file
17
src/index.css
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Cormorant Garamond", "Playfair Display", Georgia, serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import LuxuryBirthdayLanding from "./App";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<LuxuryBirthdayLanding />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
12
tailwind.config.js
Normal file
12
tailwind.config.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ["./index.html", "./src/**/*.{ts,tsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
serif: ["'Cormorant Garamond'", "'Playfair Display'", "Georgia", "serif"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
base: "./",
|
||||||
|
plugins: [react()],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user