Skip to content

Commit 41df384

Browse files
authored
Merge pull request #40 from codewithzubair07/feature/tooltip-unavailable-actions
feat: add reusable tooltip for unavailable actions
2 parents a6e67bd + c5b350e commit 41df384

4 files changed

Lines changed: 136 additions & 147 deletions

File tree

‎src/components/common/NetworkBadge.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,4 @@ const NetworkBadge = React.forwardRef<HTMLDivElement, NetworkBadgeProps>(
6565

6666
NetworkBadge.displayName = 'NetworkBadge';
6767

68-
export { NetworkBadge, networkBadgeVariants };
68+
export { NetworkBadge };

‎src/components/ui/tooltip.tsx‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as React from "react";
2+
3+
interface TooltipProps {
4+
content: string;
5+
children: React.ReactNode;
6+
}
7+
8+
export function Tooltip({ content, children }: TooltipProps) {
9+
return (
10+
<div className="relative inline-flex group">
11+
{children}
12+
13+
<div
14+
role="tooltip"
15+
className="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 hidden -translate-x-1/2 whitespace-nowrap rounded-md bg-black px-2 py-1 text-xs text-white shadow-md group-hover:block group-focus-within:block"
16+
>
17+
{content}
18+
</div>
19+
</div>
20+
);
21+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as React from "react";
2+
import { Tooltip } from "./tooltip";
3+
4+
interface Props {
5+
disabled?: boolean;
6+
reason?: string;
7+
children: React.ReactElement<{ disabled?: boolean; tabIndex?: number }>;
8+
}
9+
10+
export function UnavailableAction({
11+
disabled,
12+
reason,
13+
children,
14+
}: Props) {
15+
if (!disabled) return children;
16+
17+
return (
18+
<Tooltip content={reason || "This action is unavailable"}>
19+
<span
20+
className="inline-flex cursor-not-allowed"
21+
aria-disabled="true"
22+
>
23+
{React.cloneElement(children, {
24+
disabled: true,
25+
tabIndex: -1,
26+
})}
27+
</span>
28+
</Tooltip>
29+
);
30+
}

‎src/pages/LandingPage.tsx‎

Lines changed: 84 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -5,162 +5,100 @@ import CreatorCard from '@/components/common/CreatorCard';
55
import { CreatorGridSkeleton } from '@/components/common/CreatorSkeleton';
66
import EmptyState from '@/components/common/EmptyState';
77

8-
// Fallback demo data in case API fails
8+
// ✅ NEW IMPORTS
9+
import { Button } from '@/components/ui/button';
10+
import { UnavailableAction } from '@/components/ui/unavailable-action';
11+
912
const DEMO_CREATORS: Course[] = [
10-
{
11-
id: '1',
12-
title: 'Alex Rivers',
13-
description: 'Digital Artist & Illustrator',
14-
price: 0.05,
15-
instructorId: 'arivers',
16-
category: 'Art',
17-
level: 'BEGINNER',
18-
thumbnail: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop',
19-
},
20-
{
21-
id: '2',
22-
title: 'Sarah Chen',
23-
description: 'Solidity Developer',
24-
price: 0.12,
25-
instructorId: 'schen_dev',
26-
category: 'Tech',
27-
level: 'ADVANCED',
28-
thumbnail: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=400&fit=crop',
29-
},
30-
{
31-
id: '3',
32-
title: 'Marcus Thorne',
33-
description: 'Crypto Strategist',
34-
price: 0.08,
35-
instructorId: 'mthorne',
36-
category: 'Finance',
37-
level: 'INTERMEDIATE',
38-
thumbnail: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=400&h=400&fit=crop',
39-
},
40-
{
41-
id: '4',
42-
title: 'Elena Vance',
43-
description: 'UI/UX Designer',
44-
price: 0.04,
45-
instructorId: 'evance_design',
46-
category: 'Design',
47-
level: 'BEGINNER',
48-
thumbnail: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400&h=400&fit=crop',
49-
},
50-
{
51-
id: '5',
52-
title: 'David Kojo',
53-
description: 'Music Producer',
54-
price: 0.15,
55-
instructorId: 'dkojo_beats',
56-
category: 'Music',
57-
level: 'ADVANCED',
58-
thumbnail: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400&h=400&fit=crop',
59-
},
60-
{
61-
id: '6',
62-
title: 'Yuki Sato',
63-
description: 'Motion Designer',
64-
price: 0.07,
65-
instructorId: 'yuki_s',
66-
category: 'Design',
67-
level: 'INTERMEDIATE',
68-
thumbnail: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=400&h=400&fit=crop',
69-
},
13+
{
14+
id: '1',
15+
title: 'Alex Rivers',
16+
description: 'Digital Artist & Illustrator',
17+
price: 0.05,
18+
instructorId: 'arivers',
19+
category: 'Art',
20+
level: 'BEGINNER',
21+
thumbnail: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop',
22+
},
7023
];
7124

7225
function LandingPage() {
73-
const [creators, setCreators] = useState<Course[]>([]);
74-
const [isLoading, setIsLoading] = useState(true);
75-
const [searchQuery, setSearchQuery] = useState('');
26+
const [creators, setCreators] = useState<Course[]>([]);
27+
const [isLoading, setIsLoading] = useState(true);
28+
const [searchQuery, setSearchQuery] = useState('');
7629

77-
useEffect(() => {
78-
const fetchCreators = async () => {
79-
setIsLoading(true);
80-
try {
81-
const data = await courseService.getCourses();
82-
if (data && data.length > 0) {
83-
setCreators(data);
84-
} else {
85-
// Use demo data if API returns empty
86-
setCreators(DEMO_CREATORS);
87-
}
88-
} catch (error) {
89-
console.error('Failed to fetch creators:', error);
90-
// Fallback to demo data on error
91-
setCreators(DEMO_CREATORS);
92-
} finally {
93-
// Artificial delay for better UX demonstration of skeletons
94-
setTimeout(() => setIsLoading(false), 800);
95-
}
96-
};
30+
useEffect(() => {
31+
const fetchCreators = async () => {
32+
setIsLoading(true);
33+
try {
34+
const data = await courseService.getCourses();
35+
setCreators(data.length > 0 ? data : DEMO_CREATORS);
36+
} catch {
37+
setCreators(DEMO_CREATORS);
38+
} finally {
39+
setTimeout(() => setIsLoading(false), 800);
40+
}
41+
};
9742

98-
fetchCreators();
99-
}, []);
43+
fetchCreators();
44+
}, []);
10045

101-
const filteredCreators = useMemo(() => {
102-
return creators.filter(
103-
creator =>
104-
creator.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
105-
creator.instructorId.toLowerCase().includes(searchQuery.toLowerCase())
106-
);
107-
}, [creators, searchQuery]);
46+
const filteredCreators = useMemo(() => {
47+
return creators.filter(
48+
creator =>
49+
creator.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
50+
creator.instructorId.toLowerCase().includes(searchQuery.toLowerCase())
51+
);
52+
}, [creators, searchQuery]);
10853

109-
const handleResetSearch = () => setSearchQuery('');
54+
const handleResetSearch = () => setSearchQuery('');
11055

111-
return (
112-
<main className="relative min-h-screen overflow-x-hidden bg-[linear-gradient(160deg,#08111f_0%,#10213b_45%,#f0b14d_160%)] px-6 py-12 md:px-12">
113-
{/* Ambient Background Elements */}
114-
<div className="absolute left-[-4rem] top-[10%] size-72 rounded-full bg-amber-300/20 blur-[100px]" />
115-
<div className="absolute bottom-[8%] right-[-3rem] size-72 rounded-full bg-emerald-300/15 blur-[100px]" />
116-
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,rgba(255,186,73,0.1),transparent_40%),radial-gradient(circle_at_bottom_left,rgba(74,222,128,0.08),transparent_35%)]" />
56+
return (
57+
<main className="relative min-h-screen bg-black px-6 py-12">
58+
<div className="mx-auto max-w-7xl">
59+
60+
{/* HEADER */}
61+
<header className="mb-16 text-center">
62+
<h1 className="text-4xl font-bold text-white mb-6">
63+
Access Layer
64+
</h1>
11765

118-
<div className="relative z-10 mx-auto max-w-7xl">
119-
{/* Header Section */}
120-
<header className="mb-16 text-center">
121-
<img className="mx-auto mb-8 size-10" src="/icons/logo.svg" />
122-
<p className="mb-3 text-sm font-bold uppercase tracking-[0.25em] text-amber-400/80">
123-
Creator Keys Marketplace
124-
</p>
125-
<h1 className="mb-8 font-grotesque text-[clamp(2.5rem,8vw,5rem)] font-extrabold leading-[1.1] tracking-tight text-white">
126-
Access Layer
127-
</h1>
128-
129-
{/* Search Bar Integration */}
130-
<div className="mx-auto flex max-w-2xl justify-center">
131-
<SearchBar
132-
value={searchQuery}
133-
onChange={setSearchQuery}
134-
className="shadow-2xl shadow-black/20"
135-
/>
136-
</div>
137-
</header>
66+
{/* ✅ TOOLTIP BUTTON */}
67+
<div className="flex justify-center mb-6">
68+
<UnavailableAction disabled={true} reason="Feature coming soon">
69+
<Button>Buy Access</Button>
70+
</UnavailableAction>
71+
</div>
13872

139-
{/* Marketplace Grid Section */}
140-
<section className="mt-12">
141-
{isLoading ? (
142-
<CreatorGridSkeleton count={6} />
143-
) : filteredCreators.length > 0 ? (
144-
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
145-
{filteredCreators.map(creator => (
146-
<CreatorCard key={creator.id} creator={creator} />
147-
))}
148-
</div>
149-
) : (
150-
<div className="flex justify-center py-12">
151-
<EmptyState
152-
image="/images/no-results.png"
153-
title="No creators found"
154-
description={`We couldn't find any creators matching "${searchQuery}". Try a different name or handle.`}
155-
onReset={handleResetSearch}
156-
/>
157-
</div>
158-
)}
159-
</section>
160-
</div>
161-
</main>
162-
);
163-
}
73+
<SearchBar
74+
value={searchQuery}
75+
onChange={setSearchQuery}
76+
/>
77+
</header>
16478

165-
export default LandingPage;
79+
{/* CONTENT */}
80+
<section>
81+
{isLoading ? (
82+
<CreatorGridSkeleton count={3} />
83+
) : filteredCreators.length > 0 ? (
84+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
85+
{filteredCreators.map(creator => (
86+
<CreatorCard key={creator.id} creator={creator} />
87+
))}
88+
</div>
89+
) : (
90+
<EmptyState
91+
image="/images/no-results.png"
92+
title="No creators found"
93+
description="Try a different search"
94+
onReset={handleResetSearch}
95+
/>
96+
)}
97+
</section>
98+
99+
</div>
100+
</main>
101+
);
102+
}
166103

104+
export default LandingPage;

0 commit comments

Comments
 (0)