Skip to content
Closed
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
24 changes: 24 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,30 @@
/>
<meta name="twitter:image" content="https://accesslayer.xyz/images/og-image.png" />

<!--
#1085: apply the saved (or system) theme before first paint so dark
mode users never see a light flash. Mirrors src/lib/theme.ts —
keep the storage key and class name in sync.
-->
<script>
(function () {
var theme;
try {
theme = localStorage.getItem('accesslayer.theme');
} catch (e) {}
if (theme !== 'light' && theme !== 'dark') {
theme =
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
var root = document.documentElement;
if (theme === 'dark') root.classList.add('dark');
root.style.colorScheme = theme;
})();
</script>

<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/icons/logo.svg" />

Expand Down
91 changes: 91 additions & 0 deletions src/components/common/AddToCompareButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Check, GitCompareArrows } from 'lucide-react';
import { Tooltip } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import {
COMPARE_LIMIT_REASON,
MAX_COMPARE_KEYS,
useCompareStore,
type CompareKey,
} from '@/hooks/useCompareStore';

interface AddToCompareButtonProps {
compareKey: CompareKey;
className?: string;
}

const BASE_CLASS =
'inline-flex items-center gap-1.5 rounded-sm border px-2.5 py-1.5 font-mono text-[9px] uppercase tracking-wider transition-colors outline-none focus-visible:ring-2 focus-visible:ring-gray-400 dark:focus-visible:ring-white/50';

export default function AddToCompareButton({
compareKey,
className,
}: AddToCompareButtonProps) {
const isSelected = useCompareStore(state =>
state.keys.some(key => key.id === compareKey.id)
);
const isFull = useCompareStore(
state => state.keys.length >= MAX_COMPARE_KEYS
);
const add = useCompareStore(state => state.add);
const remove = useCompareStore(state => state.remove);

if (isSelected) {
return (
<button
type="button"
aria-pressed="true"
aria-label={`Remove ${compareKey.title} from compare`}
onClick={() => remove(compareKey.id)}
className={cn(
BASE_CLASS,
'border-gray-900 dark:border-white bg-gray-900 dark:bg-white text-white dark:text-gray-900 hover:bg-gray-700 dark:hover:bg-white/85',
className
)}
>
<Check className="size-3" aria-hidden="true" />
Comparing
</button>
);
}

if (isFull) {
// aria-disabled (not `disabled`) keeps the button focusable so
// keyboard users can still reach the tooltip explaining why.
return (
<Tooltip content={COMPARE_LIMIT_REASON}>
<button
type="button"
aria-pressed="false"
aria-disabled="true"
aria-label={`Add ${compareKey.title} to compare`}
onClick={event => event.preventDefault()}
className={cn(
BASE_CLASS,
'cursor-not-allowed border-gray-200 dark:border-white/15 text-gray-300 dark:text-white/30',
className
)}
>
<GitCompareArrows className="size-3" aria-hidden="true" />
Compare
</button>
</Tooltip>
);
}

return (
<button
type="button"
aria-pressed="false"
aria-label={`Add ${compareKey.title} to compare`}
onClick={() => add(compareKey)}
className={cn(
BASE_CLASS,
'border-gray-200 dark:border-white/15 text-gray-600 dark:text-white/70 hover:border-gray-900 dark:hover:border-white hover:text-gray-900 dark:hover:text-white',
className
)}
>
<GitCompareArrows className="size-3" aria-hidden="true" />
Compare
</button>
);
}
87 changes: 87 additions & 0 deletions src/components/common/CompareBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { ArrowRight, X } from 'lucide-react';
import { Link } from 'react-router';
import CreatorInitialsAvatar from '@/components/common/CreatorInitialsAvatar';
import { MAX_COMPARE_KEYS, useCompareStore } from '@/hooks/useCompareStore';

/**
* Sticky bottom bar listing the keys queued for comparison (#1086).
* Hidden until at least one key is selected.
*/
export default function CompareBar() {
const keys = useCompareStore(state => state.keys);
const remove = useCompareStore(state => state.remove);
const clear = useCompareStore(state => state.clear);

if (keys.length === 0) return null;

const canCompare = keys.length >= 2;

return (
<>
{/* Reserves space so the fixed bar never covers the page footer. */}
<div aria-hidden="true" className="h-20" />
<aside
aria-label="Key comparison"
className="fixed inset-x-0 bottom-0 z-40 border-t border-black/8 dark:border-white/10 bg-white/95 dark:bg-[#06111f]/95 backdrop-blur-md"
>
<div className="mx-auto flex max-w-5xl flex-wrap items-center gap-3 px-6 py-3">
<p className="font-mono text-[10px] uppercase tracking-wider text-gray-500 dark:text-white/60">
Compare {keys.length}/{MAX_COMPARE_KEYS}
</p>

<ul className="flex flex-1 flex-wrap items-center gap-2">
{keys.map(key => (
<li
key={key.id}
className="flex items-center gap-2 rounded-full border border-gray-200 dark:border-white/15 bg-gray-50 dark:bg-white/5 py-1 pr-1 pl-1"
>
<span className="size-6 overflow-hidden rounded-full">
<CreatorInitialsAvatar
name={key.title}
creatorId={key.id}
imageSrc={key.thumbnail}
className="text-[9px]"
/>
</span>
<span className="max-w-[9rem] truncate font-jakarta text-xs font-medium text-gray-900 dark:text-white">
{key.title}
</span>
<button
type="button"
onClick={() => remove(key.id)}
aria-label={`Remove ${key.title} from compare`}
className="inline-flex size-6 items-center justify-center rounded-full text-gray-400 dark:text-white/55 transition-colors hover:bg-gray-200 dark:hover:bg-white/10 hover:text-gray-900 dark:hover:text-white"
>
<X className="size-3.5" aria-hidden="true" />
</button>
</li>
))}
</ul>

<div className="flex items-center gap-2">
<button
type="button"
onClick={clear}
className="rounded-sm px-3 py-2 font-mono text-[10px] uppercase tracking-wider text-gray-500 dark:text-white/60 transition-colors hover:text-gray-900 dark:hover:text-white"
>
Clear all
</button>
{canCompare ? (
<Link
to="/compare"
className="inline-flex items-center gap-2 rounded-sm bg-gray-900 dark:bg-white px-4 py-2 font-mono text-[10px] uppercase tracking-wider text-white dark:text-gray-900 transition-colors hover:bg-gray-700 dark:hover:bg-white/85"
>
Compare keys
<ArrowRight className="size-3.5" aria-hidden="true" />
</Link>
) : (
<span className="font-jakarta text-xs text-gray-400 dark:text-white/55">
Add one more key to compare
</span>
)}
</div>
</div>
</aside>
</>
);
}
32 changes: 32 additions & 0 deletions src/components/common/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Moon, Sun } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useTheme } from '@/hooks/useTheme';

interface ThemeToggleProps {
className?: string;
}

/** Nav bar light/dark switch. Stores the choice as a manual override. */
export default function ThemeToggle({ className }: ThemeToggleProps) {
const { isDark, toggleTheme } = useTheme();
const label = isDark ? 'Switch to light mode' : 'Switch to dark mode';

return (
<button
type="button"
onClick={toggleTheme}
aria-label={label}
title={label}
className={cn(
'inline-flex size-9 items-center justify-center rounded-full text-white/70 transition-colors outline-none hover:bg-white/10 hover:text-white focus-visible:ring-2 focus-visible:ring-white/50',
className
)}
>
{isDark ? (
<Sun className="size-4" aria-hidden="true" />
) : (
<Moon className="size-4" aria-hidden="true" />
)}
</button>
);
}
9 changes: 8 additions & 1 deletion src/components/common/WalletStatusChip.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useAccount, useChainId, useDisconnect } from 'wagmi';
import { Link } from 'react-router';
import { Copy, Check, LogOut } from 'lucide-react';
import { Copy, Check, History, LogOut } from 'lucide-react';
import { useState } from 'react';
import {
DropdownMenu,
Expand Down Expand Up @@ -97,6 +97,13 @@ export function WalletStatusChip({ className = '' }: { className?: string }) {
{copied ? 'Copied' : 'Copy address'}
</DropdownMenuItem>

<DropdownMenuItem asChild>
<Link to="/transactions">
<History className="mr-2 size-4" />
Transaction history
</Link>
</DropdownMenuItem>

<DropdownMenuItem
onSelect={() => disconnect()}
className="text-red-600 focus:text-red-600"
Expand Down
32 changes: 16 additions & 16 deletions src/components/home/CreatorSpotlight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function SpotlightSkeletonCard() {
<div
role="status"
aria-label="Loading spotlight creator"
className="relative overflow-hidden rounded-2xl border border-black/8 bg-white p-5"
className="relative overflow-hidden rounded-2xl border border-black/8 dark:border-white/10 bg-white dark:bg-white/[0.03] p-5"
>
<span className="sr-only">Loading spotlight creator</span>

Expand Down Expand Up @@ -74,24 +74,24 @@ function SpotlightCreatorCard({ creator, rank }: SpotlightCreatorCardProps) {
return (
<Link
to={`/creator/${creator.id}`}
className="group relative block overflow-hidden rounded-2xl border border-black/8 bg-white p-5 transition-all duration-300 hover:-translate-y-0.5 hover:border-gray-300 hover:shadow-[0_12px_32px_-16px_rgba(0,0,0,0.15)]"
className="group relative block overflow-hidden rounded-2xl border border-black/8 dark:border-white/10 bg-white dark:bg-white/[0.03] p-5 transition-all duration-300 hover:-translate-y-0.5 hover:border-gray-300 dark:hover:border-white/25 hover:shadow-[0_12px_32px_-16px_rgba(0,0,0,0.15)]"
>
{/* Rank badge */}
<div className="mb-3 flex items-center gap-1.5">
{rank === 1 ? (
<span className="inline-flex items-center gap-1 rounded-full bg-amber-100 px-2.5 py-0.5 font-jakarta text-[10px] font-bold uppercase tracking-wider text-amber-700">
<span className="inline-flex items-center gap-1 rounded-full bg-amber-100 dark:bg-amber-400/15 px-2.5 py-0.5 font-jakarta text-[10px] font-bold uppercase tracking-wider text-amber-700 dark:text-amber-300">
<Star className="size-3 fill-amber-500 text-amber-500" />
Top
</span>
) : (
<span className="inline-flex items-center justify-center rounded-full bg-gray-100 px-2.5 py-0.5 font-jakarta text-[10px] font-bold text-gray-500">
<span className="inline-flex items-center justify-center rounded-full bg-gray-100 dark:bg-white/10 px-2.5 py-0.5 font-jakarta text-[10px] font-bold text-gray-500 dark:text-white/60">
#{rank}
</span>
)}
</div>

{/* Avatar */}
<div className="relative mb-4 aspect-square w-full overflow-hidden rounded-xl bg-gray-100">
<div className="relative mb-4 aspect-square w-full overflow-hidden rounded-xl bg-gray-100 dark:bg-white/10">
<CreatorInitialsAvatar
name={name}
creatorId={creator.id}
Expand All @@ -101,22 +101,22 @@ function SpotlightCreatorCard({ creator, rank }: SpotlightCreatorCardProps) {
</div>

{/* Name */}
<h3 className="truncate font-jakarta text-base font-semibold text-gray-900">
<h3 className="truncate font-jakarta text-base font-semibold text-gray-900 dark:text-white">
{name}
</h3>

{/* Stats row */}
<div className="mt-3 flex items-center justify-between border-t border-gray-100 pt-3">
<div className="mt-3 flex items-center justify-between border-t border-gray-100 dark:border-white/10 pt-3">
{/* Key price */}
<div className="flex items-center gap-1.5">
<span className="font-jakarta text-sm font-semibold text-gray-900">
<span className="font-jakarta text-sm font-semibold text-gray-900 dark:text-white">
{keyPrice}
</span>
<span className="font-jakarta text-[11px] text-gray-400">XLM</span>
<span className="font-jakarta text-[11px] text-gray-400 dark:text-white/55">XLM</span>
</div>

{/* Holder count */}
<div className="flex items-center gap-1.5 text-gray-400">
<div className="flex items-center gap-1.5 text-gray-400 dark:text-white/55">
<Users className="size-3.5" />
<span className="font-mono text-[10px] tabular-nums">
{formatHolderCount(holderCount)}
Expand All @@ -125,7 +125,7 @@ function SpotlightCreatorCard({ creator, rank }: SpotlightCreatorCardProps) {
</div>

{/* CTA */}
<div className="mt-3 flex items-center gap-1 font-mono text-[9px] uppercase tracking-wider text-gray-400 transition-colors group-hover:text-gray-900">
<div className="mt-3 flex items-center gap-1 font-mono text-[9px] uppercase tracking-wider text-gray-400 dark:text-white/55 transition-colors group-hover:text-gray-900 dark:group-hover:text-white">
View profile
<ArrowRight className="size-3 transition-transform group-hover:translate-x-0.5" />
</div>
Expand Down Expand Up @@ -171,30 +171,30 @@ export default function CreatorSpotlight() {
}

return (
<section className="bg-gradient-to-b from-white to-gray-50/50 px-6 py-20">
<section className="bg-gradient-to-b from-white to-gray-50/50 dark:from-[#06111f] dark:to-[#081627] px-6 py-20">
<div className="mx-auto max-w-5xl">
{/* Header */}
<div ref={headingRef} className="scroll-reveal">
<div className="flex items-center gap-2">
<span className="size-1.5 rounded-full bg-amber-400" />
<span className="font-jakarta text-sm font-medium text-amber-600">
<span className="font-jakarta text-sm font-medium text-amber-600 dark:text-amber-400">
Creator Spotlight
</span>
</div>

<div className="mt-3 flex items-end justify-between gap-6">
<h2 className="font-pt-serif text-[clamp(1.6rem,3.5vw,2.4rem)] font-normal leading-[1.15]">
<span className="text-gray-900">
<span className="text-gray-900 dark:text-white">
Most collected creators
</span>
<br />
<span className="text-gray-400">
<span className="text-gray-400 dark:text-white/55">
discover who has the biggest following.
</span>
</h2>
<Link
to="/marketplace"
className="mb-1 hidden shrink-0 items-center gap-2 font-mono text-[10px] uppercase tracking-wider text-gray-400 transition-colors hover:text-gray-900 md:flex"
className="mb-1 hidden shrink-0 items-center gap-2 font-mono text-[10px] uppercase tracking-wider text-gray-400 dark:text-white/55 transition-colors hover:text-gray-900 dark:hover:text-white md:flex"
>
View all
<ArrowRight className="size-3.5" />
Expand Down
Loading
Loading