mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-21 04:01:41 +10:00
refactor(v4.0.0-alpha): beginning of a new era
This commit is contained in:
46
apps/client/src/pages/home/sections/hero/call-to-action.tsx
Normal file
46
apps/client/src/pages/home/sections/hero/call-to-action.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { Book, SignOut } from "@phosphor-icons/react";
|
||||
import { Button } from "@reactive-resume/ui";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { useLogout } from "@/client/services/auth";
|
||||
import { useAuthStore } from "@/client/stores/auth";
|
||||
|
||||
export const HeroCTA = () => {
|
||||
const { logout } = useLogout();
|
||||
|
||||
const isLoggedIn = useAuthStore((state) => !!state.user);
|
||||
|
||||
if (isLoggedIn) {
|
||||
return (
|
||||
<>
|
||||
<Button asChild size="lg">
|
||||
<Link to="/dashboard">Go to Dashboard</Link>
|
||||
</Button>
|
||||
|
||||
<Button size="lg" variant="link" onClick={() => logout()}>
|
||||
<SignOut className="mr-3" />
|
||||
Logout
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<>
|
||||
<Button asChild size="lg">
|
||||
<Link to="/auth/login">Get started</Link>
|
||||
</Button>
|
||||
|
||||
<Button asChild size="lg" variant="link">
|
||||
<a href="https://docs.rxresu.me" target="_blank" rel="noopener noreferrer nofollow">
|
||||
<Book className="mr-3" />
|
||||
Learn more
|
||||
</a>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
47
apps/client/src/pages/home/sections/hero/decoration.tsx
Normal file
47
apps/client/src/pages/home/sections/hero/decoration.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
export const Decoration = {
|
||||
Grid: () => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 -z-10 h-full w-full stroke-foreground/10 opacity-60 [mask-image:radial-gradient(100%_100%_at_top_right,white,transparent)] dark:opacity-40"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="983e3e4c-de6d-4c3f-8d64-b9761d1534cc"
|
||||
width={200}
|
||||
height={200}
|
||||
x="50%"
|
||||
y={-1}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<path d="M.5 200V.5H200" fill="none" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<svg x="50%" y={-1} className="overflow-visible fill-border/20">
|
||||
<path
|
||||
d="M-200 0h201v201h-201Z M600 0h201v201h-201Z M-400 600h201v201h-201Z M200 800h201v201h-201Z"
|
||||
strokeWidth={0}
|
||||
/>
|
||||
</svg>
|
||||
<rect
|
||||
width="100%"
|
||||
height="100%"
|
||||
strokeWidth={0}
|
||||
fill="url(#983e3e4c-de6d-4c3f-8d64-b9761d1534cc)"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
Gradient: () => (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute left-[calc(50%-4rem)] top-10 -z-10 transform-gpu blur-3xl sm:left-[calc(50%-18rem)] lg:left-48 lg:top-[calc(50%-30rem)] xl:left-[calc(50%-24rem)]"
|
||||
>
|
||||
<div
|
||||
className="aspect-[1108/632] h-96 w-[69.25rem] bg-gradient-to-r from-[#6f8cbb] to-[#c93b37] opacity-40 dark:opacity-20"
|
||||
style={{
|
||||
clipPath:
|
||||
"polygon(73.6% 51.7%, 91.7% 11.8%, 100% 46.4%, 97.4% 82.2%, 92.5% 84.9%, 75.7% 64%, 55.3% 47.5%, 46.5% 49.4%, 45% 62.9%, 50.3% 87.2%, 21.3% 64.1%, 0.1% 100%, 5.4% 51.1%, 21.4% 63.9%, 58.9% 0.2%, 73.6% 51.7%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
65
apps/client/src/pages/home/sections/hero/index.tsx
Normal file
65
apps/client/src/pages/home/sections/hero/index.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { ArrowRight } from "@phosphor-icons/react";
|
||||
import { Badge, Button } from "@reactive-resume/ui";
|
||||
import { motion } from "framer-motion";
|
||||
import Tilt from "react-parallax-tilt";
|
||||
|
||||
import { defaultTiltProps } from "@/client/constants/parallax-tilt";
|
||||
|
||||
import { HeroCTA } from "./call-to-action";
|
||||
import { Decoration } from "./decoration";
|
||||
|
||||
export const HeroSection = () => (
|
||||
<section className="relative">
|
||||
<Decoration.Grid />
|
||||
<Decoration.Gradient />
|
||||
|
||||
<div className="mx-auto max-w-7xl px-6 lg:flex lg:h-screen lg:items-center lg:px-12">
|
||||
<motion.div
|
||||
className="mx-auto max-w-3xl shrink-0 lg:mx-0 lg:max-w-xl lg:pt-8"
|
||||
initial={{ opacity: 0, x: -100 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
>
|
||||
<div className="mt-24 flex items-center gap-x-4 sm:mt-32 lg:mt-0">
|
||||
<Badge>Version 4</Badge>
|
||||
|
||||
<Button variant="link" className="space-x-2 text-left">
|
||||
<p>What's new in the latest version</p>
|
||||
<ArrowRight />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 space-y-2">
|
||||
<h6 className="text-base font-bold tracking-wide">Finally,</h6>
|
||||
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl">
|
||||
A free and open-source resume builder.
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<p className="prose prose-base prose-zinc mt-6 text-lg leading-8 dark:prose-invert">
|
||||
A free and open-source resume builder that simplifies the process of creating, updating,
|
||||
and sharing your resume.
|
||||
</p>
|
||||
|
||||
<div className="mt-10 flex items-center gap-x-8">
|
||||
<HeroCTA />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="mx-auto mt-16 flex max-w-2xl sm:mt-24 lg:ml-10 lg:mr-0 lg:mt-0 lg:max-w-none lg:flex-none xl:ml-20">
|
||||
<div className="max-w-3xl flex-none sm:max-w-5xl lg:max-w-none">
|
||||
<motion.div initial={{ opacity: 0, x: 100 }} whileInView={{ opacity: 1, x: 0 }}>
|
||||
<Tilt {...defaultTiltProps}>
|
||||
<img
|
||||
width={3600}
|
||||
height={2078}
|
||||
src="/screenshots/builder.png"
|
||||
alt="Reactive Resume - Screenshot - Builder Screen"
|
||||
className="w-[76rem] rounded-lg bg-background/5 shadow-2xl ring-1 ring-foreground/10"
|
||||
/>
|
||||
</Tilt>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
60
apps/client/src/pages/home/sections/logo-cloud/index.tsx
Normal file
60
apps/client/src/pages/home/sections/logo-cloud/index.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { Button } from "@reactive-resume/ui";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
|
||||
type LogoProps = { company: string };
|
||||
|
||||
const Logo = ({ company }: LogoProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"col-span-2 col-start-2 sm:col-start-auto lg:col-span-1",
|
||||
company === "twilio" && "sm:col-start-2",
|
||||
)}
|
||||
>
|
||||
{/* Show on Light Theme */}
|
||||
<img
|
||||
className="block max-h-12 object-contain dark:hidden"
|
||||
src={`/brand-logos/dark/${company}.svg`}
|
||||
alt={company}
|
||||
width={212}
|
||||
height={48}
|
||||
/>
|
||||
{/* Show on Dark Theme */}
|
||||
<img
|
||||
className="hidden max-h-12 object-contain dark:block"
|
||||
src={`/brand-logos/light/${company}.svg`}
|
||||
alt={company}
|
||||
width={212}
|
||||
height={48}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const logoList: string[] = ["amazon", "google", "postman", "twilio", "zalando"];
|
||||
|
||||
export const LogoCloudSection = () => (
|
||||
<section className="relative py-24 sm:py-32">
|
||||
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<p className="text-center text-lg leading-relaxed">
|
||||
Reactive Resume has helped people land jobs at these great companies:
|
||||
</p>
|
||||
<div className="mx-auto mt-10 grid max-w-lg grid-cols-4 items-center gap-x-8 gap-y-10 sm:max-w-xl sm:grid-cols-6 sm:gap-x-10 lg:mx-0 lg:max-w-none lg:grid-cols-5">
|
||||
{logoList.map((company) => (
|
||||
<Logo key={company} company={company} />
|
||||
))}
|
||||
</div>
|
||||
<p className="mx-auto mt-8 max-w-sm text-center leading-relaxed">
|
||||
If this app has helped you with your job hunt, let me know by reaching out through{" "}
|
||||
<Button asChild variant="link" className="p-0">
|
||||
<a
|
||||
href="https://www.amruthpillai.com/#contact"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
this contact form
|
||||
</a>
|
||||
</Button>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
57
apps/client/src/pages/home/sections/statistics/counter.tsx
Normal file
57
apps/client/src/pages/home/sections/statistics/counter.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { animate, motion } from "framer-motion";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
type CounterProps = { from: number; to: number };
|
||||
|
||||
export const Counter = ({ from, to }: CounterProps) => {
|
||||
const [isInView, setIsInView] = useState(false);
|
||||
const nodeRef = useRef<HTMLParagraphElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const node = nodeRef.current;
|
||||
|
||||
if (!node) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsInView(true);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1 },
|
||||
);
|
||||
|
||||
observer.observe(node);
|
||||
|
||||
return () => {
|
||||
observer.unobserve(node);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInView) return;
|
||||
|
||||
const node = nodeRef.current;
|
||||
if (!node) return;
|
||||
|
||||
const controls = animate(from, to, {
|
||||
duration: 1,
|
||||
onUpdate(value) {
|
||||
node.textContent = Math.round(value).toLocaleString();
|
||||
},
|
||||
});
|
||||
|
||||
return () => controls.stop();
|
||||
}, [from, to, isInView]);
|
||||
|
||||
return (
|
||||
<motion.span
|
||||
ref={nodeRef}
|
||||
transition={{ duration: 0.5 }}
|
||||
initial={{ opacity: 0, scale: 0.1 }}
|
||||
animate={isInView ? { opacity: 1, scale: 1 } : {}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
29
apps/client/src/pages/home/sections/statistics/index.tsx
Normal file
29
apps/client/src/pages/home/sections/statistics/index.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { Counter } from "./counter";
|
||||
|
||||
type Statistic = {
|
||||
name: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
const stats: Statistic[] = [
|
||||
{ name: "GitHub Stars", value: 11800 },
|
||||
{ name: "Users Signed Up", value: 300000 },
|
||||
{ name: "Resumes Generated", value: 400000 },
|
||||
];
|
||||
|
||||
export const StatisticsSection = () => (
|
||||
<section className="relative py-24 sm:py-32">
|
||||
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<dl className="grid grid-cols-1 gap-x-8 gap-y-16 text-center lg:grid-cols-3">
|
||||
{stats.map((stat, index) => (
|
||||
<div key={index} className="mx-auto flex max-w-xs flex-col gap-y-4">
|
||||
<dt className="text-base leading-7 opacity-60">{stat.name}</dt>
|
||||
<dd className="order-first text-3xl font-semibold tracking-tight sm:text-5xl">
|
||||
<Counter from={0} to={stat.value} />+
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
Reference in New Issue
Block a user