Skip to content

Commit e6f06e4

Browse files
authored
Merge pull request #177 from DevanshuNEU/feature/167-design-system-landing-page
feat(design-system): Python-native theme and core landing page components
2 parents 35d57a4 + 544b1d3 commit e6f06e4

7 files changed

Lines changed: 715 additions & 0 deletions

File tree

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { HTMLAttributes, useState } from 'react';
2+
import { motion, AnimatePresence } from 'framer-motion';
3+
import { cn } from '@/lib/utils';
4+
import { syntax, codeBg } from '@/lib/python-theme';
5+
import { lineHighlightVariants } from '@/lib/animations';
6+
import { Check, Copy, ExternalLink } from 'lucide-react';
7+
8+
interface CodeBlockProps extends HTMLAttributes<HTMLDivElement> {
9+
code: string;
10+
language?: 'python' | 'text';
11+
filename?: string;
12+
lineStart?: number;
13+
highlightLines?: number[];
14+
showLineNumbers?: boolean;
15+
maxHeight?: string;
16+
onCopy?: () => void;
17+
githubUrl?: string;
18+
}
19+
20+
type Token = { type: string; value: string };
21+
22+
// Dead simple tokenizer - not trying to be a full parser, just good enough for display
23+
function tokenizePython(code: string): Token[] {
24+
const tokens: Token[] = [];
25+
26+
// Order matters here - more specific patterns first
27+
const patterns: [string, RegExp][] = [
28+
['comment', /^#.*/],
29+
['docstring', /^("""[\s\S]*?"""|'''[\s\S]*?''')/],
30+
['fstring', /^f(['"])((?:\\.|(?!\1)[^\\])*)\1/],
31+
['string', /^(['"])((?:\\.|(?!\1)[^\\])*)\1/],
32+
['decorator', /^@[\w.]+/],
33+
['keyword', /^\b(def|class|import|from|return|if|elif|else|for|while|try|except|finally|with|as|yield|lambda|pass|break|continue|raise|assert|global|nonlocal|del|in|not|and|or|is|True|False|None|async|await)\b/],
34+
['builtin', /^\b(print|len|range|str|int|float|list|dict|set|tuple|bool|type|isinstance|hasattr|getattr|setattr|open|input|super|self|cls)\b/],
35+
['number', /^\b\d+(\.\d+)?\b/],
36+
['function', /^\b([a-zA-Z_][a-zA-Z0-9_]*)(?=\s*\()/],
37+
['className', /^\b([A-Z][a-zA-Z0-9_]*)\b/],
38+
['parameter', /^\b([a-zA-Z_][a-zA-Z0-9_]*)(?=\s*[=:])/],
39+
['operator', /^(==|!=|<=|>=|<|>|\+|-|\*|\/|%|\*\*|=|\+=|-=|\|\||&&)/],
40+
['punctuation', /^[()[\]{}:,.;]/],
41+
['variable', /^[a-zA-Z_][a-zA-Z0-9_]*/],
42+
['whitespace', /^\s+/],
43+
];
44+
45+
let remaining = code;
46+
while (remaining.length > 0) {
47+
let matched = false;
48+
for (const [type, pattern] of patterns) {
49+
const match = remaining.match(pattern);
50+
if (match) {
51+
tokens.push({ type, value: match[0] });
52+
remaining = remaining.slice(match[0].length);
53+
matched = true;
54+
break;
55+
}
56+
}
57+
if (!matched) {
58+
tokens.push({ type: 'text', value: remaining[0] });
59+
remaining = remaining.slice(1);
60+
}
61+
}
62+
63+
return tokens;
64+
}
65+
66+
const tokenColors: Record<string, string> = {
67+
keyword: syntax.keyword,
68+
builtin: syntax.builtin,
69+
function: syntax.function,
70+
className: syntax.className,
71+
decorator: syntax.decorator,
72+
string: syntax.string,
73+
fstring: syntax.string,
74+
docstring: syntax.docstring,
75+
comment: syntax.comment,
76+
number: syntax.number,
77+
parameter: syntax.parameter,
78+
operator: syntax.operator,
79+
punctuation: syntax.punctuation,
80+
variable: syntax.variable,
81+
};
82+
83+
// Python code block with syntax highlighting and copy button
84+
export function CodeBlock({
85+
code,
86+
language = 'python',
87+
filename,
88+
lineStart = 1,
89+
highlightLines = [],
90+
showLineNumbers = true,
91+
maxHeight = '400px',
92+
onCopy,
93+
githubUrl,
94+
className,
95+
...props
96+
}: CodeBlockProps) {
97+
const [copied, setCopied] = useState(false);
98+
const lines = code.split('\n');
99+
100+
const handleCopy = async () => {
101+
await navigator.clipboard.writeText(code);
102+
setCopied(true);
103+
onCopy?.();
104+
setTimeout(() => setCopied(false), 2000);
105+
};
106+
107+
return (
108+
<div
109+
className={cn('rounded-lg overflow-hidden border border-white/[0.08]', className)}
110+
style={{ backgroundColor: codeBg.elevated }}
111+
{...props}
112+
>
113+
{(filename || githubUrl) && (
114+
<div
115+
className="px-4 py-2 flex items-center justify-between border-b border-white/[0.08]"
116+
style={{ backgroundColor: codeBg.primary }}
117+
>
118+
{filename && (
119+
<span className="text-sm text-text-secondary font-mono">{filename}</span>
120+
)}
121+
<div className="flex items-center gap-2">
122+
<button
123+
onClick={handleCopy}
124+
className="p-1.5 rounded hover:bg-white/[0.05] transition-colors text-text-muted hover:text-text-primary"
125+
title="Copy code"
126+
>
127+
<AnimatePresence mode="wait">
128+
<motion.div
129+
key={copied ? 'check' : 'copy'}
130+
initial={{ scale: 0.5, opacity: 0 }}
131+
animate={{ scale: 1, opacity: 1 }}
132+
exit={{ scale: 0.5, opacity: 0 }}
133+
>
134+
{copied ? (
135+
<Check size={16} className="text-green-500" />
136+
) : (
137+
<Copy size={16} />
138+
)}
139+
</motion.div>
140+
</AnimatePresence>
141+
</button>
142+
{githubUrl && (
143+
<a
144+
href={githubUrl}
145+
target="_blank"
146+
rel="noopener noreferrer"
147+
className="p-1.5 rounded hover:bg-white/[0.05] transition-colors text-text-muted hover:text-text-primary"
148+
title="View on GitHub"
149+
>
150+
<ExternalLink size={16} />
151+
</a>
152+
)}
153+
</div>
154+
</div>
155+
)}
156+
157+
<div className="overflow-auto scrollbar-thin" style={{ maxHeight }}>
158+
<pre className="p-4 text-sm font-mono leading-relaxed">
159+
<code>
160+
{lines.map((line, index) => {
161+
const lineNumber = lineStart + index;
162+
const isHighlighted = highlightLines.includes(lineNumber);
163+
const tokens = language === 'python'
164+
? tokenizePython(line)
165+
: [{ type: 'text', value: line }];
166+
167+
return (
168+
<motion.div
169+
key={index}
170+
className={cn('flex', isHighlighted && 'rounded')}
171+
variants={lineHighlightVariants}
172+
initial="idle"
173+
whileHover="hover"
174+
style={{
175+
backgroundColor: isHighlighted ? syntax.matchHighlight : undefined,
176+
}}
177+
>
178+
{showLineNumbers && (
179+
<span
180+
className="select-none pr-4 text-right min-w-[3rem]"
181+
style={{
182+
color: isHighlighted ? syntax.lineNumberActive : syntax.lineNumber,
183+
}}
184+
>
185+
{lineNumber}
186+
</span>
187+
)}
188+
<span className="flex-1">
189+
{tokens.map((token, i) => (
190+
<span key={i} style={{ color: tokenColors[token.type] || syntax.variable }}>
191+
{token.value}
192+
</span>
193+
))}
194+
{line === '' && '\u200B'}
195+
</span>
196+
</motion.div>
197+
);
198+
})}
199+
</code>
200+
</pre>
201+
</div>
202+
</div>
203+
);
204+
}
205+
206+
export default CodeBlock;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { forwardRef, HTMLAttributes, ReactNode } from 'react';
2+
import { motion, HTMLMotionProps } from 'framer-motion';
3+
import { cn } from '@/lib/utils';
4+
import { glassCardVariants } from '@/lib/animations';
5+
6+
export type GlowColor = 'blue' | 'yellow' | 'none';
7+
8+
interface GlassCardProps extends Omit<HTMLMotionProps<'div'>, 'children'> {
9+
children: ReactNode;
10+
glow?: GlowColor;
11+
hover?: boolean;
12+
className?: string;
13+
as?: 'div' | 'article' | 'section';
14+
}
15+
16+
const glowStyles: Record<GlowColor, string> = {
17+
blue: 'hover:shadow-[0_0_30px_rgba(75,139,190,0.15)]',
18+
yellow: 'hover:shadow-[0_0_30px_rgba(255,212,59,0.15)]',
19+
none: '',
20+
};
21+
22+
// Glassmorphism card with optional Python-colored glow
23+
export const GlassCard = forwardRef<HTMLDivElement, GlassCardProps>(
24+
({ children, glow = 'none', hover = true, className, as = 'div', ...props }, ref) => {
25+
const Component = motion[as] as typeof motion.div;
26+
27+
return (
28+
<Component
29+
ref={ref}
30+
className={cn(
31+
'rounded-xl bg-white/[0.03] backdrop-blur-xl border border-white/[0.08]',
32+
'transition-all duration-normal ease-out-expo',
33+
hover && 'hover:bg-white/[0.05] hover:border-white/[0.12]',
34+
glow !== 'none' && glowStyles[glow],
35+
className
36+
)}
37+
variants={hover ? glassCardVariants : undefined}
38+
initial={hover ? 'idle' : undefined}
39+
whileHover={hover ? 'hover' : undefined}
40+
{...props}
41+
>
42+
{children}
43+
</Component>
44+
);
45+
}
46+
);
47+
48+
GlassCard.displayName = 'GlassCard';
49+
50+
// Compound components
51+
52+
interface GlassCardContentProps extends HTMLAttributes<HTMLDivElement> {
53+
children: ReactNode;
54+
padding?: 'sm' | 'md' | 'lg';
55+
}
56+
57+
const paddingSizes = { sm: 'p-3', md: 'p-4', lg: 'p-6' };
58+
59+
export const GlassCardContent = ({
60+
children,
61+
padding = 'md',
62+
className,
63+
...props
64+
}: GlassCardContentProps) => (
65+
<div className={cn(paddingSizes[padding], className)} {...props}>
66+
{children}
67+
</div>
68+
);
69+
70+
export const GlassCardHeader = ({
71+
children,
72+
className,
73+
...props
74+
}: HTMLAttributes<HTMLDivElement>) => (
75+
<div
76+
className={cn(
77+
'px-4 py-3 border-b border-white/[0.08] flex items-center justify-between',
78+
className
79+
)}
80+
{...props}
81+
>
82+
{children}
83+
</div>
84+
);
85+
86+
export const GlassCardFooter = ({
87+
children,
88+
className,
89+
...props
90+
}: HTMLAttributes<HTMLDivElement>) => (
91+
<div
92+
className={cn(
93+
'px-4 py-3 border-t border-white/[0.08] flex items-center gap-2',
94+
className
95+
)}
96+
{...props}
97+
>
98+
{children}
99+
</div>
100+
);
101+
102+
export default GlassCard;

frontend/src/hooks/useIsMobile.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useState, useEffect } from 'react';
2+
3+
const MOBILE_BREAKPOINT = 768;
4+
5+
export function useIsMobile(): boolean {
6+
const [isMobile, setIsMobile] = useState(false);
7+
8+
useEffect(() => {
9+
if (typeof window === 'undefined') return;
10+
11+
const check = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
12+
check();
13+
14+
// Debounce resize to avoid thrashing
15+
let timeout: NodeJS.Timeout;
16+
const onResize = () => {
17+
clearTimeout(timeout);
18+
timeout = setTimeout(check, 100);
19+
};
20+
21+
window.addEventListener('resize', onResize);
22+
return () => {
23+
window.removeEventListener('resize', onResize);
24+
clearTimeout(timeout);
25+
};
26+
}, []);
27+
28+
return isMobile;
29+
}
30+
31+
export default useIsMobile;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useState, useEffect } from 'react';
2+
3+
// Respects user's OS-level motion preferences
4+
export function usePrefersReducedMotion(): boolean {
5+
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
6+
7+
useEffect(() => {
8+
if (typeof window === 'undefined') return;
9+
10+
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
11+
setPrefersReducedMotion(mq.matches);
12+
13+
const onChange = (e: MediaQueryListEvent) => setPrefersReducedMotion(e.matches);
14+
15+
mq.addEventListener?.('change', onChange) ?? mq.addListener?.(onChange);
16+
return () => {
17+
mq.removeEventListener?.('change', onChange) ?? mq.removeListener?.(onChange);
18+
};
19+
}, []);
20+
21+
return prefersReducedMotion;
22+
}
23+
24+
export default usePrefersReducedMotion;

0 commit comments

Comments
 (0)