Skip to content

Commit 1dfbc87

Browse files
Merge branch 'main' into issue-286
2 parents 71b4bf3 + 22e0e92 commit 1dfbc87

23 files changed

Lines changed: 1956 additions & 215 deletions

.env.example

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
1+
VITE_BACKEND_URL=/api
2+
# UTM settings for shared profile links (optional)
3+
# Set any of these to enable UTM parameters on shared profile URLs.
4+
VITE_UTM_SOURCE=twitter
5+
VITE_UTM_MEDIUM=social
6+
VITE_UTM_CAMPAIGN=share_profile
7+
#VITE_UTM_TERM=
8+
#VITE_UTM_CONTENT=
19
VITE_BACKEND_URL=http://localhost:3000/api/v1
210
VITE_DEFAULT_CHAIN_ID=84532
311
VITE_ANVIL_RPC_URL=http://127.0.0.1:8545
412
VITE_BASE_SEPOLIA_RPC_URL=https://sepolia.base.org
513
VITE_SEPOLIA_RPC_URL=
614
VITE_MAINNET_RPC_URL=
15+
16+
# UTM settings for shared profile links (optional)
17+
# Set any of these to enable UTM parameters on shared profile URLs.
18+
# Remove or comment out to disable UTM tracking.
19+
# Example configuration:
20+
VITE_UTM_SOURCE=accesslayer
21+
VITE_UTM_MEDIUM=share
22+
VITE_UTM_CAMPAIGN=profile-sharing
23+
# VITE_UTM_TERM=
24+
# VITE_UTM_CONTENT=

.husky/pre-commit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
#!/usr/bin/env sh
22
sh ./scripts/check-no-package-lock.sh
3-
pnpm lint-staged
3+
npx lint-staged
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { cn } from '@/lib/utils';
2+
import { Button } from '@/components/ui/button';
3+
import { SlidersHorizontal, ArrowRight } from 'lucide-react';
4+
5+
interface ClearedFiltersEmptyStateProps {
6+
/**
7+
* Called when the user clicks "Browse all creators".
8+
* Typically resets sort to 'featured' and clears any residual state.
9+
*/
10+
onBrowseAll?: () => void;
11+
className?: string;
12+
}
13+
14+
/**
15+
* Shown when all filters have been cleared but the creator list is still
16+
* empty. Distinct from the search-no-results empty state — this state means
17+
* the marketplace itself has no listings, not that a specific query failed.
18+
*/
19+
const ClearedFiltersEmptyState: React.FC<ClearedFiltersEmptyStateProps> = ({
20+
onBrowseAll,
21+
className,
22+
}) => (
23+
<div
24+
className={cn(
25+
'flex flex-col items-center justify-center rounded-[2rem] border border-white/10 bg-white/5 px-8 py-14 text-center backdrop-blur-xl',
26+
className
27+
)}
28+
role="status"
29+
aria-label="No creators available"
30+
>
31+
<div className="relative mb-6 flex size-20 items-center justify-center">
32+
<div className="absolute inset-0 size-full rounded-full bg-amber-500/10 blur-2xl" />
33+
<span className="relative z-10 flex size-16 items-center justify-center rounded-full border border-white/10 bg-white/5">
34+
<SlidersHorizontal
35+
className="size-7 text-white/40"
36+
aria-hidden="true"
37+
/>
38+
</span>
39+
</div>
40+
41+
<h2 className="font-grotesque text-2xl font-black tracking-tight text-white mb-2">
42+
Nothing here yet
43+
</h2>
44+
<p className="max-w-[300px] font-jakarta text-sm leading-relaxed text-white/50 mb-8">
45+
Your filters are cleared, but no creators are available right now.
46+
Check back soon or try browsing all categories.
47+
</p>
48+
49+
{onBrowseAll && (
50+
<Button
51+
onClick={onBrowseAll}
52+
variant="outline"
53+
aria-label="Browse all creators"
54+
className="rounded-xl border-white/10 bg-white/5 px-6 font-bold text-white transition-all hover:border-amber-500/30 hover:bg-amber-500/10"
55+
>
56+
Browse all creators
57+
<ArrowRight className="ml-2 size-4" aria-hidden="true" />
58+
</Button>
59+
)}
60+
</div>
61+
);
62+
63+
export default ClearedFiltersEmptyState;

src/components/common/CreatorCard.tsx

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@ import { useRef, useState } from 'react';
22
import { useAccount } from 'wagmi';
33
import type { Course } from '@/services/course.service';
44
import { cn } from '@/lib/utils';
5-
import { ShoppingCart, Link as LinkIcon, TrendingUp } from 'lucide-react';
5+
import { ShoppingCart, Link as LinkIcon, TrendingUp, MoreVertical, Copy, Share2, ExternalLink } from 'lucide-react';
6+
import {
7+
DropdownMenu,
8+
DropdownMenuContent,
9+
DropdownMenuItem,
10+
DropdownMenuLabel,
11+
DropdownMenuSeparator,
12+
DropdownMenuTrigger,
13+
} from '@/components/ui/dropdown-menu';
14+
import RecentActivityBadge from '@/components/common/RecentActivityBadge';
615
import toast from 'react-hot-toast';
716
import showToast from '@/utils/toast.util';
817
import { formatCompactNumber, formatNumber } from '@/utils/numberFormat.utils';
@@ -27,6 +36,7 @@ import CreatorListRowDivider from '@/components/common/CreatorListRowDivider';
2736
import BuyActionHelperText from '@/components/common/BuyActionHelperText';
2837
import NetworkFeeHint from '@/components/common/NetworkFeeHint';
2938
import CreatorBio from '@/components/common/CreatorBio';
39+
import { CREATOR_CARD_MEDIA_RADIUS_CLASS } from '@/utils/creatorCardTokens';
3040

3141
interface CreatorCardProps {
3242
creator: Course;
@@ -105,6 +115,28 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
105115
}, 1500);
106116
};
107117

118+
const isRecentlyActive = (creator.volume24h ?? 0) > 0;
119+
120+
const handleCopyLink = () => {
121+
const url = `${window.location.origin}/creator/${creator.id}`;
122+
navigator.clipboard
123+
.writeText(url)
124+
.then(() => toast.success('Profile link copied'))
125+
.catch(() => toast.error('Could not copy link'));
126+
};
127+
128+
const handleShare = () => {
129+
const url = `${window.location.origin}/creator/${creator.id}`;
130+
if (navigator.share) {
131+
navigator.share({ title: creator.title, url }).catch(() => {});
132+
} else {
133+
navigator.clipboard
134+
.writeText(url)
135+
.then(() => toast.success('Link copied to clipboard'))
136+
.catch(() => toast.error('Could not share'));
137+
}
138+
};
139+
108140
const handleBuy = () => {
109141
if (!isConnected) {
110142
toast.error('Please connect your wallet to purchase keys', {
@@ -134,8 +166,51 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
134166
className
135167
)}
136168
>
169+
<div className="absolute right-3 top-3 z-20">
170+
<DropdownMenu>
171+
<DropdownMenuTrigger
172+
aria-label={`More actions for ${creator.title}`}
173+
className="flex size-8 items-center justify-center rounded-full text-white/40 transition-colors hover:bg-white/10 hover:text-white/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/60"
174+
>
175+
<MoreVertical className="size-4" aria-hidden="true" />
176+
</DropdownMenuTrigger>
177+
<DropdownMenuContent
178+
align="end"
179+
className="w-48 border-white/10 bg-slate-900/95 backdrop-blur-xl"
180+
>
181+
<DropdownMenuLabel className="text-xs text-white/50">
182+
{creator.title}
183+
</DropdownMenuLabel>
184+
<DropdownMenuSeparator className="bg-white/10" />
185+
<DropdownMenuItem
186+
onSelect={handleCopyLink}
187+
className="cursor-pointer gap-2 text-white/70 focus:bg-white/10 focus:text-white"
188+
>
189+
<Copy className="size-3.5" aria-hidden="true" />
190+
Copy profile link
191+
</DropdownMenuItem>
192+
<DropdownMenuItem
193+
onSelect={handleShare}
194+
className="cursor-pointer gap-2 text-white/70 focus:bg-white/10 focus:text-white"
195+
>
196+
<Share2 className="size-3.5" aria-hidden="true" />
197+
Share creator
198+
</DropdownMenuItem>
199+
<DropdownMenuItem
200+
onSelect={() => {}}
201+
className="cursor-pointer gap-2 text-white/70 focus:bg-white/10 focus:text-white"
202+
>
203+
<ExternalLink className="size-3.5" aria-hidden="true" />
204+
View profile
205+
</DropdownMenuItem>
206+
</DropdownMenuContent>
207+
</DropdownMenu>
208+
</div>
137209
<div
138-
className="relative mb-4 aspect-square overflow-hidden rounded-xl"
210+
className={cn(
211+
'relative mb-4 aspect-square overflow-hidden',
212+
CREATOR_CARD_MEDIA_RADIUS_CLASS
213+
)}
139214
role="img"
140215
aria-labelledby={`creator-name-${creator.id}`}
141216
>
@@ -176,6 +251,7 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
176251
/>
177252
<Change24hBadge change={creator.change24h} />
178253
<KeySupplyBadge supply={creator.creatorShareSupply} />
254+
{isRecentlyActive && <RecentActivityBadge />}
179255
</div>
180256
<p className="marketplace-label-muted font-jakarta text-sm">
181257
{displayInstructorHandle}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { cn } from '@/lib/utils';
2+
import { Button } from '@/components/ui/button';
3+
import { ChevronLeft, ChevronRight } from 'lucide-react';
4+
5+
interface CreatorListPaginationProps {
6+
/** 0-indexed current page. */
7+
page: number;
8+
totalPages: number;
9+
onPageChange: (page: number) => void;
10+
className?: string;
11+
}
12+
13+
const CreatorListPagination: React.FC<CreatorListPaginationProps> = ({
14+
page,
15+
totalPages,
16+
onPageChange,
17+
className,
18+
}) => {
19+
const displayPage = page + 1;
20+
21+
return (
22+
<nav
23+
aria-label="Creator list pagination"
24+
className={cn('flex items-center justify-center gap-3', className)}
25+
>
26+
{/*
27+
* Live region announces the new page to screen readers on every
28+
* navigation. aria-atomic ensures the full "Page X of Y" phrase
29+
* is read rather than just the changed number.
30+
*/}
31+
<div
32+
aria-live="polite"
33+
aria-atomic="true"
34+
className="sr-only"
35+
>
36+
{`Page ${displayPage} of ${totalPages}`}
37+
</div>
38+
39+
<Button
40+
type="button"
41+
variant="outline"
42+
size="sm"
43+
disabled={page === 0}
44+
onClick={() => onPageChange(Math.max(0, page - 1))}
45+
aria-label="Go to previous page"
46+
>
47+
<ChevronLeft className="size-4" aria-hidden="true" />
48+
<span aria-hidden="true">Previous</span>
49+
</Button>
50+
51+
{/*
52+
* aria-hidden suppresses the visual "Page X of Y" from screen
53+
* readers — the sr-only live region above handles announcements
54+
* to avoid duplicate readout.
55+
*/}
56+
<span
57+
className="marketplace-label-muted text-xs tabular-nums"
58+
aria-hidden="true"
59+
>
60+
Page {displayPage} of {totalPages}
61+
</span>
62+
63+
<Button
64+
type="button"
65+
variant="outline"
66+
size="sm"
67+
disabled={page >= totalPages - 1}
68+
onClick={() => onPageChange(Math.min(totalPages - 1, page + 1))}
69+
aria-label="Go to next page"
70+
>
71+
<span aria-hidden="true">Next</span>
72+
<ChevronRight className="size-4" aria-hidden="true" />
73+
</Button>
74+
</nav>
75+
);
76+
};
77+
78+
export default CreatorListPagination;

0 commit comments

Comments
 (0)