Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/components/common/CreatorCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import toast from 'react-hot-toast';
import showToast from '@/utils/toast.util';
import { formatCompactNumber } from '@/utils/numberFormat.utils';
import { formatCreatorKeyPriceDisplay } from '@/utils/keyPriceDisplay.utils';
import CreatorDropCountdown from '@/components/common/CreatorDropCountdown';
import { formatCreatorHandle } from '@/utils/handleDisplay.utils';
import { formatJoinDate } from '@/utils/formatJoinDate';
import { useSystemTheme } from '@/utils/useSystemTheme';
import { Tooltip } from '@/components/ui/tooltip';
import { AsyncButton } from '@/components/ui/async-button';
import { useNetworkMismatch } from '@/hooks/useNetworkMismatch';
import { useTransactionTelemetry } from '@/hooks/useTransactionTelemetry';
Expand All @@ -38,6 +40,7 @@ import CreatorListRowDivider from '@/components/common/CreatorListRowDivider';
import BuyActionHelperText from '@/components/common/BuyActionHelperText';
import NetworkFeeHint from '@/components/common/NetworkFeeHint';
import CreatorBio from '@/components/common/CreatorBio';
import CreatorDropCountdown from '@/components/common/CreatorDropCountdown';
import CreatorHandleHoverCard from '@/components/common/CreatorHandleHoverCard';
import { CREATOR_CARD_MEDIA_RADIUS_CLASS } from '@/utils/creatorCardTokens';

Expand All @@ -59,6 +62,7 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
className,
isPriceRefreshing = false,
}) => {
const { isDarkMode } = useSystemTheme();
// Display-normalised handles. Raw values stay on `creator` for any
// equality / URL logic downstream.
const displayInstructorHandle =
Expand Down Expand Up @@ -222,7 +226,7 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
name={creator.title}
creatorId={creator.id}
imageSrc={creator.thumbnail}
imageClassName="transition-transform duration-500 motion-reduce:transition-none motion-safe:md:group-hover:scale-[1.03] motion-reduce:md:group-hover:scale-100"
imageClassName={cn("transition-transform duration-500 motion-reduce:transition-none motion-safe:md:group-hover:scale-[1.03] motion-reduce:md:group-hover:scale-100", isDarkMode ? 'bg-gray-800' : 'bg-gray-100')}
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/80 via-transparent to-transparent opacity-0 transition-opacity duration-300 motion-reduce:transition-none motion-safe:md:group-hover:opacity-100 motion-reduce:md:group-hover:opacity-100" />
{creator.volume24h !== undefined && (
Expand Down Expand Up @@ -315,6 +319,18 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
</div>
<CreatorListRowDivider className="my-4" />
<div className="mt-3 space-y-2">
{creator.joinedAt && (
<CardMetaRow
label="Join Date"
value={
<Tooltip content={<p>{new Date(creator.joinedAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</p>}>
<span className="text-white/75 cursor-default truncate">
{formatJoinDate(creator.joinedAt)}
</span>
</Tooltip>
}
/>
)}
<CardMetaRow
label={
<span className="inline-flex items-center gap-1.5">
Expand Down
61 changes: 61 additions & 0 deletions src/components/common/MarketplaceSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { useState } from 'react';
import { Menu, X } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';

interface MarketplaceSidebarProps {
className?: string;
}

const MarketplaceSidebar: React.FC<MarketplaceSidebarProps> = ({ className }) => {
const [isOpen, setIsOpen] = useState(false);

return (
<>
{/* Mobile Toggle Button */}
<div className="md:hidden fixed top-4 left-4 z-50">
<Button
variant="outline"
size="icon"
onClick={() => setIsOpen(!isOpen)}
aria-label={isOpen ? 'Close sidebar' : 'Open sidebar'}
aria-expanded={isOpen}
className="bg-slate-900 border-white/10"
>
{isOpen ? <X className="size-5" /> : <Menu className="size-5" />}
</Button>
</div>

{/* Sidebar Overlay */}
{isOpen && (
<div
className="md:hidden fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"
onClick={() => setIsOpen(false)}
aria-hidden="true"
/>
)}

{/* Sidebar Content */}
<aside
className={cn(
'fixed inset-y-0 left-0 z-40 w-64 transform bg-slate-950 border-r border-white/10 transition-transform duration-300 ease-in-out md:translate-x-0 md:static md:w-64',
isOpen ? 'translate-x-0' : '-translate-x-full',
className
)}
>
<div className="p-6 h-full flex flex-col">
<div className="mb-8">
<h2 className="text-xl font-black text-white tracking-tight">Navigation</h2>
</div>
<nav className="flex-1 space-y-2">
<a href="/" className="block px-4 py-2 text-sm text-white/70 hover:text-white hover:bg-white/5 rounded-lg transition-colors">Home</a>
<a href="/creators" className="block px-4 py-2 text-sm text-white/70 hover:text-white hover:bg-white/5 rounded-lg transition-colors">Creators</a>
<a href="/activity" className="block px-4 py-2 text-sm text-white/70 hover:text-white hover:bg-white/5 rounded-lg transition-colors">Activity</a>
</nav>
</div>
</aside>
</>
);
};

export default MarketplaceSidebar;
21 changes: 20 additions & 1 deletion src/components/common/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Search } from 'lucide-react';
import { useRef } from 'react';
import { cn } from '@/lib/utils';
import InlineValidationMessage from '@/components/common/InlineValidationMessage';

Expand All @@ -19,6 +20,13 @@ const SearchBar: React.FC<SearchBarProps> = ({
validationMessage,
isLoading = false,
}) => {
const inputRef = useRef<HTMLInputElement>(null);

const handleClear = () => {
onChange('');
inputRef.current?.focus();
};

if (isLoading) {
return (
<div className={cn('w-full max-w-md', className)}>
Expand All @@ -41,16 +49,27 @@ const SearchBar: React.FC<SearchBarProps> = ({
<Search className="size-5 text-white/50" aria-hidden="true" />
</div>
<input
ref={inputRef}
type="text"
className={cn(
'block w-full rounded-xl border border-white/10 bg-white/5 py-3 pl-10 pr-3 text-sm text-white placeholder:text-white/40 focus:border-amber-500/50 focus:bg-white/10 focus:outline-none focus:ring-2 focus:ring-amber-500/20',
'block w-full rounded-xl border border-white/10 bg-white/5 py-3 pl-10 pr-10 text-sm text-white placeholder:text-white/40 focus:border-amber-500/50 focus:bg-white/10 focus:outline-none focus:ring-2 focus:ring-amber-500/20',
validationMessage &&
'border-amber-400/45 focus:border-amber-400/65 focus:ring-amber-300/20'
)}
placeholder={placeholder}
value={value}
onChange={e => onChange(e.target.value)}
/>
{value && (
<button
type="button"
aria-label="Clear search"
onClick={handleClear}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-white/50 hover:text-white transition-colors"
>
</button>
)}
</div>
{validationMessage && (
<InlineValidationMessage message={validationMessage} />
Expand Down
2 changes: 2 additions & 0 deletions src/pages/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { CREATOR_LIST_SORT_LAYOUT_TRANSITION } from '@/utils/creatorListSortTran
import { AlertCircle, ChevronDown, RefreshCw } from 'lucide-react';
import ClearedFiltersEmptyState from '@/components/common/ClearedFiltersEmptyState';
import CreatorListPagination from '@/components/common/CreatorListPagination';
import MarketplaceSidebar from '@/components/common/MarketplaceSidebar';

const FEATURED_CREATOR_FACTS = [
{ label: 'Membership', value: 'Collectors Circle' },
Expand Down Expand Up @@ -537,6 +538,7 @@ function LandingPage() {
<div className="absolute left-[-4rem] top-[10%] size-72 rounded-full bg-amber-300/20 blur-[100px]" />
<div className="absolute bottom-[8%] right-[-3rem] size-72 rounded-full bg-emerald-300/15 blur-[100px]" />
<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%)]" />
<MarketplaceSidebar />
<div className="relative z-10 mx-auto max-w-7xl">
<MarketplaceSection
as="header"
Expand Down
1 change: 1 addition & 0 deletions src/services/course.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface Course {
isVerified?: boolean;
volume24h?: number;
change24h?: number;
joinedAt?: string;
}

export interface GetCoursesParams {
Expand Down
25 changes: 25 additions & 0 deletions src/utils/formatJoinDate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export function formatJoinDate(dateString: string): string {
const date = new Date(dateString);
const now = new Date();

const diffTime = Math.abs(now.getTime() - date.getTime());
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

if (diffDays === 0) {
return 'Joined today';
}

if (diffDays < 30) {
return `Joined ${rtf.format(-diffDays, 'day').replace(' ago', ' ago')}`;
}

const diffMonths = Math.floor(diffDays / 30);
if (diffMonths < 12) {
return `Joined ${rtf.format(-diffMonths, 'month')}`;
}

const diffYears = Math.floor(diffDays / 365);
return `Joined ${rtf.format(-diffYears, 'year')}`;
}
23 changes: 23 additions & 0 deletions src/utils/useSystemTheme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useState, useEffect } from 'react';

export function useSystemTheme() {
const [isDarkMode, setIsDarkMode] = useState<boolean>(() => {
if (typeof window === 'undefined') return false;
return window.matchMedia('(prefers-color-scheme: dark)').matches;
});

useEffect(() => {
if (typeof window === 'undefined') return;

const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

const callback = (e: MediaQueryListEvent) => {
setIsDarkMode(e.matches);
};

mediaQuery.addEventListener('change', callback);
return () => mediaQuery.removeEventListener('change', callback);
}, []);

return { isDarkMode };
}
Loading