Skip to content

Commit 049946d

Browse files
committed
feat: implement hero section with responsive layout and tests
Fix some bugs
1 parent 3512e9a commit 049946d

13 files changed

Lines changed: 1081 additions & 678 deletions

File tree

next.config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/** @type {import('next').NextConfig} */
22
const nextConfig = {
3-
output: 'export',
43
images: {
54
unoptimized: true,
65
},

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"test:coverage": "jest --coverage"
1515
},
1616
"dependencies": {
17+
"@radix-ui/react-slot": "^1.2.3",
1718
"class-variance-authority": "^0.7.1",
1819
"clsx": "^2.1.1",
1920
"lucide-react": "^0.511.0",

public/avatar.png

1.52 MB
Loading

src/app/page.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,29 @@ import {
1111
Section,
1212
Heading,
1313
Text,
14+
Hero,
1415
} from "@/components/ui";
1516
import { ThemeToggle } from '../components/ui/ThemeToggle'
1617

1718
export default function Home() {
1819
return (
1920
<div className="min-h-screen bg-background text-text">
21+
<Hero
22+
title="Senior Full-Stack Developer"
23+
description="Building modern web applications with React, Next.js, and TypeScript. Passionate about creating beautiful, performant, and accessible user experiences."
24+
location="San Francisco, CA"
25+
avatarSrc="/avatar.png"
26+
avatarAlt="Profile picture"
27+
ctaPrimary={{
28+
text: "Contact Me",
29+
link: "/contact",
30+
}}
31+
ctaSecondary={{
32+
text: "View Work",
33+
link: "/work",
34+
}}
35+
/>
36+
2037
<Section variant="surface" spacing="lg" className="border-b border-accent/20">
2138
<div className="container mx-auto flex justify-between items-center">
2239
<Heading as="h1" size="h2" className="text-accent">UI Component Showcase</Heading>

src/components/ui/Button.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { cva, type VariantProps } from 'class-variance-authority';
22
import { cn } from '@/lib/utils';
33
import { Loader2 } from 'lucide-react';
4-
import React from 'react';
4+
import React, { forwardRef } from 'react';
5+
import { Slot } from "@radix-ui/react-slot";
56

67
const buttonVariants = cva(
78
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
@@ -36,12 +37,14 @@ export interface ButtonProps
3637
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
3738
VariantProps<typeof buttonVariants> {
3839
isLoading?: boolean;
40+
asChild?: boolean;
3941
}
4042

41-
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
42-
({ className, variant, size, isLoading, children, ...props }, ref) => {
43+
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
44+
({ className, variant, size, isLoading, children, asChild = false, ...props }, ref) => {
45+
const Comp = asChild ? Slot : "button";
4346
return (
44-
<button
47+
<Comp
4548
className={cn(buttonVariants({ variant, size, className }))}
4649
ref={ref}
4750
disabled={isLoading}
@@ -55,7 +58,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
5558
) : (
5659
children
5760
)}
58-
</button>
61+
</Comp>
5962
);
6063
}
6164
);

src/components/ui/Hero.tsx

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { Button } from "./Button";
2+
import { Heading, Text } from "./Typography";
3+
import { Section } from "./Section";
4+
import { Badge } from "./Badge";
5+
import Link from "next/link";
6+
import Image from "next/image";
7+
8+
interface HeroProps {
9+
title: string;
10+
description: string;
11+
location?: string;
12+
avatarSrc?: string;
13+
avatarAlt?: string;
14+
ctaPrimary?: {
15+
text: string;
16+
link: string;
17+
};
18+
ctaSecondary?: {
19+
text: string;
20+
link: string;
21+
};
22+
className?: string;
23+
}
24+
25+
export function Hero({
26+
title,
27+
description,
28+
location,
29+
avatarSrc,
30+
avatarAlt = "Profile picture",
31+
ctaPrimary,
32+
ctaSecondary,
33+
className = "",
34+
}: HeroProps) {
35+
return (
36+
<Section
37+
variant="surface"
38+
spacing="xl"
39+
className={`relative min-h-[calc(100vh-4rem)] flex items-center justify-center overflow-hidden ${className}`}
40+
>
41+
<div className="container mx-auto max-w-4xl text-center">
42+
{avatarSrc && (
43+
<div className="mb-8 relative w-32 h-32 mx-auto">
44+
<Image
45+
src={avatarSrc}
46+
alt={avatarAlt}
47+
fill
48+
className="rounded-full object-cover border-4 border-accent"
49+
priority
50+
/>
51+
</div>
52+
)}
53+
54+
<Heading
55+
as="h1"
56+
size="h1"
57+
className="mb-4 bg-gradient-to-r from-accent to-accent/80 bg-clip-text text-transparent"
58+
>
59+
{title}
60+
</Heading>
61+
62+
{location && (
63+
<Badge variant="secondary" className="mb-6">
64+
{location}
65+
</Badge>
66+
)}
67+
68+
<Text size="xl" className="mb-8 text-muted max-w-2xl mx-auto">
69+
{description}
70+
</Text>
71+
72+
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
73+
{ctaPrimary && (
74+
<Button
75+
asChild
76+
size="lg"
77+
className="inline-flex items-center gap-2"
78+
>
79+
<Link href={ctaPrimary.link}>
80+
{ctaPrimary.text}
81+
<svg
82+
xmlns="http://www.w3.org/2000/svg"
83+
width="20"
84+
height="20"
85+
viewBox="0 0 24 24"
86+
fill="none"
87+
stroke="currentColor"
88+
strokeWidth="2"
89+
strokeLinecap="round"
90+
strokeLinejoin="round"
91+
>
92+
<path d="M5 12h14" />
93+
<path d="m12 5 7 7-7 7" />
94+
</svg>
95+
</Link>
96+
</Button>
97+
)}
98+
99+
{ctaSecondary && (
100+
<Button
101+
asChild
102+
variant="outline"
103+
size="lg"
104+
>
105+
<Link href={ctaSecondary.link}>
106+
{ctaSecondary.text}
107+
</Link>
108+
</Button>
109+
)}
110+
</div>
111+
112+
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce">
113+
<svg
114+
data-testid="scroll-indicator"
115+
xmlns="http://www.w3.org/2000/svg"
116+
width="24"
117+
height="24"
118+
viewBox="0 0 24 24"
119+
fill="none"
120+
stroke="currentColor"
121+
strokeWidth="2"
122+
strokeLinecap="round"
123+
strokeLinejoin="round"
124+
className="text-accent"
125+
>
126+
<path d="M12 5v14" />
127+
<path d="m19 12-7 7-7-7" />
128+
</svg>
129+
</div>
130+
</div>
131+
</Section>
132+
);
133+
}

src/components/ui/Text.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { cn } from "@/lib/utils";
2+
import { VariantProps, cva } from "class-variance-authority";
3+
import { forwardRef } from "react";
4+
5+
const textVariants = cva("", {
6+
variants: {
7+
size: {
8+
xs: "text-xs",
9+
sm: "text-sm",
10+
base: "text-base",
11+
lg: "text-lg",
12+
xl: "text-xl",
13+
},
14+
variant: {
15+
default: "text-text",
16+
muted: "text-muted",
17+
subtle: "text-subtle",
18+
},
19+
weight: {
20+
normal: "font-normal",
21+
medium: "font-medium",
22+
semibold: "font-semibold",
23+
bold: "font-bold",
24+
},
25+
},
26+
defaultVariants: {
27+
size: "base",
28+
variant: "default",
29+
weight: "normal",
30+
},
31+
});
32+
33+
interface TextProps
34+
extends React.HTMLAttributes<HTMLParagraphElement>,
35+
VariantProps<typeof textVariants> {}
36+
37+
const Text = forwardRef<HTMLParagraphElement, TextProps>(
38+
({ className, size, variant, weight, ...props }, ref) => {
39+
return (
40+
<p
41+
className={cn(textVariants({ size, variant, weight, className }))}
42+
ref={ref}
43+
{...props}
44+
/>
45+
);
46+
}
47+
);
48+
49+
Text.displayName = "Text";
50+
51+
export { Text, textVariants };

0 commit comments

Comments
 (0)