Skip to content

Commit 4a2e96b

Browse files
committed
feat: add custom UI components including animated tabs and utility functions
1 parent 772652b commit 4a2e96b

File tree

3 files changed

+134
-0
lines changed

3 files changed

+134
-0
lines changed

src/components/ui/Tabs.jsx

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"use client";
2+
3+
import { useState, useEffect } from "react";
4+
import { motion } from "motion/react";
5+
import { cn } from "../../lib/utils";
6+
7+
const Tabs = ({
8+
tabs: propTabs,
9+
containerClassName,
10+
activeTabClassName,
11+
tabClassName,
12+
contentClassName,
13+
theme = 'light',
14+
}) => {
15+
const currentTheme = theme;
16+
const [active, setActive] = useState(propTabs[0]);
17+
const [tabs, setTabs] = useState(propTabs);
18+
19+
// Force re-render when theme changes
20+
useEffect(() => {
21+
setTabs([...propTabs]);
22+
}, [theme, propTabs]);
23+
24+
const moveSelectedTabToTop = (idx) => {
25+
const newTabs = [...propTabs];
26+
const selectedTab = newTabs.splice(idx, 1);
27+
newTabs.unshift(selectedTab[0]);
28+
setTabs(newTabs);
29+
setActive(newTabs[0]);
30+
};
31+
32+
const [hovering, setHovering] = useState(false);
33+
34+
return (
35+
<>
36+
<div
37+
className={cn(
38+
"flex flex-row items-center justify-start [perspective:1000px] relative overflow-auto sm:overflow-visible no-visible-scrollbar max-w-full w-full",
39+
containerClassName
40+
)}
41+
>
42+
{propTabs.map((tab, idx) => (
43+
<button
44+
key={tab.title}
45+
onClick={() => {
46+
moveSelectedTabToTop(idx);
47+
}}
48+
onMouseEnter={() => setHovering(true)}
49+
onMouseLeave={() => setHovering(false)}
50+
className={cn("relative px-4 py-2 rounded-full", tabClassName)}
51+
style={{
52+
transformStyle: "preserve-3d",
53+
}}
54+
>
55+
{active.value === tab.value && (
56+
<motion.div
57+
layoutId="clickedbutton"
58+
transition={{ type: "spring", bounce: 0.3, duration: 0.6 }}
59+
className={cn(
60+
"absolute inset-0 rounded-full",
61+
activeTabClassName
62+
)}
63+
/>
64+
)}
65+
66+
<span className={`relative block ${currentTheme === 'dark' ? 'text-white' : 'text-black'}`}>
67+
{tab.title}
68+
</span>
69+
</button>
70+
))}
71+
</div>
72+
<FadeInDiv
73+
tabs={tabs}
74+
active={active}
75+
key={active.value}
76+
hovering={hovering}
77+
className={cn("mt-8", contentClassName)}
78+
/>
79+
</>
80+
);
81+
};
82+
83+
const FadeInDiv = ({
84+
className,
85+
tabs,
86+
hovering,
87+
}) => {
88+
const isActive = (tab) => {
89+
return tab.value === tabs[0].value;
90+
};
91+
return (
92+
<div className="relative w-full h-full">
93+
{tabs.map((tab, idx) => (
94+
<motion.div
95+
key={tab.value}
96+
layoutId={tab.value}
97+
style={{
98+
scale: 1 - idx * 0.1,
99+
top: hovering ? idx * -50 : 0,
100+
zIndex: -idx,
101+
opacity: idx < 3 ? 1 - idx * 0.1 : 0,
102+
}}
103+
animate={{
104+
y: isActive(tab) ? [0, 40, 0] : 0,
105+
}}
106+
className={cn("w-full h-full absolute top-0 left-0", className)}
107+
>
108+
{tab.content}
109+
</motion.div>
110+
))}
111+
</div>
112+
);
113+
};
114+
115+
export default Tabs;

src/config/supabase.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { createClient } from '@supabase/supabase-js';
2+
3+
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
4+
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
5+
6+
if (!supabaseUrl || !supabaseAnonKey) {
7+
console.warn('VITE_SUPABASE_URL or VITE_SUPABASE_ANON_KEY environment variables are not set. Authentication will not work.');
8+
}
9+
10+
export const supabase = createClient(
11+
supabaseUrl || 'https://example.supabase.co',
12+
supabaseAnonKey || 'demo-key'
13+
);

src/lib/utils.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { clsx } from "clsx";
2+
import { twMerge } from "tailwind-merge";
3+
4+
export function cn(...inputs) {
5+
return twMerge(clsx(inputs));
6+
}

0 commit comments

Comments
 (0)