Track B — Auth & User System (complete): - User registration with bcrypt + Turnstile verification - JWT access/refresh token flow with httpOnly cookie rotation - Redis refresh token blocklist for logout - User profile + settings update endpoints (username, email) - API key generation with bcrypt hashing (ff_key_ prefix) - BYOK key management with AES-256-GCM encryption at rest - Free tier rate limiting (5 shaders/month) - Tier-gated endpoints (Pro/Studio for BYOK, API keys, bounty posting) Track C — Shader Submission & Renderer (complete): - GLSL validator: entry point check, banned extensions, infinite loop detection, brace balancing, loop bound warnings, code length limits - Puppeteer/headless Chromium renderer with Shadertoy-compatible uniform injection (iTime, iResolution, iMouse), WebGL2 with SwiftShader fallback - Shader compilation error detection via page title signaling - Thumbnail capture at t=1s, preview frame at t=duration - Renderer client service for API→renderer HTTP communication - Shader submission pipeline: validate GLSL → create record → enqueue render job - Desire fulfillment linking on shader submit - Re-validation and re-render on shader code update - Fork endpoint copies code, tags, metadata, enqueues new render Track D — Frontend Shell (complete): - React 18 + Vite + TypeScript + Tailwind CSS + TanStack Query + Zustand - Dark theme with custom fracta color palette and surface tones - Responsive layout with sticky navbar, gradient branding - Auth: Login + Register pages with JWT token management - API client with automatic 401 refresh interceptor - ShaderCanvas: Full WebGL2 renderer component with Shadertoy uniforms, mouse tracking, ResizeObserver, debounced recompilation, error callbacks - GLSL Editor: Split pane (code textarea + live preview), 400ms debounced preview, metadata panel (description, tags, type), GLSL validation errors, shader publish flow, fork-from-existing support - Feed: Infinite scroll with IntersectionObserver sentinel, dwell time tracking, skeleton loading states, empty state with CTA - Explore: Search + tag filter + sort tabs (trending/new/top), grid layout - ShaderDetail: Full-screen preview, vote controls, view source toggle, fork button - Bounties: Desire queue list sorted by heat score, status badges, tip display - BountyDetail: Single desire view with style hints, fulfill CTA - Profile: User header with avatar initial, shader grid - Settings: Account info, API key management (create/revoke/copy), subscription tiers - Generate: AI generation UI stub with prompt input, style controls, example prompts 76 files, ~5,700 lines of application code.
127 lines
4.5 KiB
TypeScript
127 lines
4.5 KiB
TypeScript
/**
|
||
* Explore page — browse shaders by tag, trending, new, top.
|
||
*/
|
||
|
||
import { useState } from 'react';
|
||
import { useQuery } from '@tanstack/react-query';
|
||
import { Link, useSearchParams } from 'react-router-dom';
|
||
import api from '@/lib/api';
|
||
import ShaderCanvas from '@/components/ShaderCanvas';
|
||
|
||
type SortOption = 'trending' | 'new' | 'top';
|
||
|
||
export default function Explore() {
|
||
const [searchParams, setSearchParams] = useSearchParams();
|
||
const [sort, setSort] = useState<SortOption>((searchParams.get('sort') as SortOption) || 'trending');
|
||
const [query, setQuery] = useState(searchParams.get('q') || '');
|
||
const tagFilter = searchParams.get('tags')?.split(',').filter(Boolean) || [];
|
||
|
||
const { data: shaders = [], isLoading } = useQuery({
|
||
queryKey: ['explore', sort, query, tagFilter.join(',')],
|
||
queryFn: async () => {
|
||
const params: any = { sort, limit: 30 };
|
||
if (query) params.q = query;
|
||
if (tagFilter.length) params.tags = tagFilter;
|
||
const { data } = await api.get('/shaders', { params });
|
||
return data;
|
||
},
|
||
});
|
||
|
||
const handleSearch = (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setSearchParams({ sort, q: query });
|
||
};
|
||
|
||
return (
|
||
<div className="max-w-7xl mx-auto px-4 py-6">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h1 className="text-xl font-semibold">Explore</h1>
|
||
|
||
{/* Sort tabs */}
|
||
<div className="flex gap-1 bg-surface-2 rounded-lg p-1">
|
||
{(['trending', 'new', 'top'] as SortOption[]).map((s) => (
|
||
<button
|
||
key={s}
|
||
onClick={() => setSort(s)}
|
||
className={`px-3 py-1 text-sm rounded-md transition-colors ${
|
||
sort === s ? 'bg-fracta-600 text-white' : 'text-gray-400 hover:text-gray-200'
|
||
}`}
|
||
>
|
||
{s.charAt(0).toUpperCase() + s.slice(1)}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Search */}
|
||
<form onSubmit={handleSearch} className="mb-6">
|
||
<input
|
||
type="text"
|
||
value={query}
|
||
onChange={(e) => setQuery(e.target.value)}
|
||
className="input max-w-md"
|
||
placeholder="Search shaders..."
|
||
/>
|
||
</form>
|
||
|
||
{/* Tag filter pills */}
|
||
{tagFilter.length > 0 && (
|
||
<div className="flex gap-2 mb-4">
|
||
{tagFilter.map((tag) => (
|
||
<span key={tag} className="text-xs px-2 py-1 bg-fracta-600/20 text-fracta-400 rounded-full flex items-center gap-1">
|
||
#{tag}
|
||
<button
|
||
onClick={() => {
|
||
const newTags = tagFilter.filter(t => t !== tag);
|
||
setSearchParams(newTags.length ? { tags: newTags.join(',') } : {});
|
||
}}
|
||
className="hover:text-white"
|
||
>
|
||
×
|
||
</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Grid */}
|
||
{isLoading ? (
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||
{Array.from({ length: 8 }).map((_, i) => (
|
||
<div key={i} className="card animate-pulse">
|
||
<div className="aspect-video bg-surface-3" />
|
||
<div className="p-3 space-y-2">
|
||
<div className="h-4 bg-surface-3 rounded w-3/4" />
|
||
<div className="h-3 bg-surface-3 rounded w-1/2" />
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : shaders.length > 0 ? (
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||
{shaders.map((shader: any) => (
|
||
<Link key={shader.id} to={`/shader/${shader.id}`} className="card group">
|
||
<div className="aspect-video bg-surface-2 overflow-hidden">
|
||
<ShaderCanvas code={shader.glsl_code} className="w-full h-full" animate={true} />
|
||
</div>
|
||
<div className="p-3">
|
||
<h3 className="font-medium text-gray-100 group-hover:text-fracta-400 transition-colors truncate">
|
||
{shader.title}
|
||
</h3>
|
||
<div className="flex items-center gap-2 mt-1 text-xs text-gray-500">
|
||
<span>{shader.shader_type}</span>
|
||
<span>·</span>
|
||
<span>{shader.view_count} views</span>
|
||
</div>
|
||
</div>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="text-center py-20 text-gray-500">
|
||
No shaders found. Try a different search or sort.
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|