forked from CSGOWTF/csgowtf
feat: implement CS2-inspired design system and UI components
This commit delivers a comprehensive design system and component library inspired by Counter-Strike 2's tactical aesthetic. Design System: - Created docs/DESIGN.md with complete design language documentation - CS2-inspired color palette: T-side orange (#d4a74a), CT-side blue (#5e98d9) - Dark-first approach with tactical, data-dense layouts - Typography scale, spacing system, and animation guidelines Component Library: - Button component: 4 variants (primary, secondary, ghost, danger), 3 sizes - Badge component: 7 variants including team-specific badges - Card component: 3 variants (default, elevated, interactive) - Header component: Responsive navigation with mobile menu - Footer component: Site-wide footer with organized link sections Pages: - Redesigned homepage with hero section, featured matches, features grid, CTA - Created placeholder pages: /matches, /players, /about - All pages follow CS2 aesthetic with proper component usage Technical Fixes: - Fixed Svelte 5 snippet syntax errors (removed incorrect render prop pattern) - Fixed Card component accessibility (conditional button/div rendering) - Removed invalid CSS border-border class from app.css - Ensured zero TypeScript errors and warnings Build Status: ✓ Verified with 0 errors, 0 warnings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
132
src/lib/components/layout/Footer.svelte
Normal file
132
src/lib/components/layout/Footer.svelte
Normal file
@@ -0,0 +1,132 @@
|
||||
<script lang="ts">
|
||||
import { Github, Heart } from 'lucide-svelte';
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const links = {
|
||||
main: [
|
||||
{ name: 'Home', href: '/' },
|
||||
{ name: 'Matches', href: '/matches' },
|
||||
{ name: 'Players', href: '/players' },
|
||||
{ name: 'API Docs', href: '/docs/api' }
|
||||
],
|
||||
about: [
|
||||
{ name: 'About', href: '/about' },
|
||||
{ name: 'FAQ', href: '/faq' },
|
||||
{ name: 'Privacy', href: '/privacy' },
|
||||
{ name: 'Terms', href: '/terms' }
|
||||
],
|
||||
resources: [
|
||||
{ name: 'GitHub', href: 'https://somegit.dev/CSGOWTF/csgowtf', external: true },
|
||||
{ name: 'Backend', href: 'https://somegit.dev/CSGOWTF/csgowtfd', external: true },
|
||||
{
|
||||
name: 'Donate',
|
||||
href: 'https://liberapay.com/CSGOWTF/',
|
||||
external: true
|
||||
}
|
||||
]
|
||||
};
|
||||
</script>
|
||||
|
||||
<footer class="border-t border-base-300 bg-base-100">
|
||||
<div class="container mx-auto px-4 py-12">
|
||||
<div class="grid gap-8 md:grid-cols-4">
|
||||
<!-- Brand -->
|
||||
<div class="md:col-span-1">
|
||||
<a href="/" class="mb-4 inline-block text-2xl font-bold">
|
||||
<span class="text-primary">CS2</span><span class="text-secondary">.WTF</span>
|
||||
</a>
|
||||
<p class="mb-4 text-sm text-base-content/60">
|
||||
Statistics for CS2 matchmaking matches. Free and open source.
|
||||
</p>
|
||||
<div class="flex gap-3">
|
||||
<a
|
||||
href="https://somegit.dev/CSGOWTF/csgowtf"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-base-content/60 transition-colors hover:text-primary"
|
||||
aria-label="GitHub"
|
||||
>
|
||||
<Github class="h-5 w-5" />
|
||||
</a>
|
||||
<a
|
||||
href="https://liberapay.com/CSGOWTF/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-base-content/60 transition-colors hover:text-error"
|
||||
aria-label="Support on Liberapay"
|
||||
>
|
||||
<Heart class="h-5 w-5" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Links -->
|
||||
<div>
|
||||
<h3 class="mb-3 text-sm font-semibold uppercase tracking-wider text-base-content/80">
|
||||
Navigate
|
||||
</h3>
|
||||
<ul class="space-y-2">
|
||||
{#each links.main as link}
|
||||
<li>
|
||||
<a
|
||||
href={link.href}
|
||||
class="text-sm text-base-content/60 transition-colors hover:text-primary"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="mb-3 text-sm font-semibold uppercase tracking-wider text-base-content/80">
|
||||
About
|
||||
</h3>
|
||||
<ul class="space-y-2">
|
||||
{#each links.about as link}
|
||||
<li>
|
||||
<a
|
||||
href={link.href}
|
||||
class="text-sm text-base-content/60 transition-colors hover:text-primary"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="mb-3 text-sm font-semibold uppercase tracking-wider text-base-content/80">
|
||||
Resources
|
||||
</h3>
|
||||
<ul class="space-y-2">
|
||||
{#each links.resources as link}
|
||||
<li>
|
||||
<a
|
||||
href={link.href}
|
||||
class="text-sm text-base-content/60 transition-colors hover:text-primary"
|
||||
{...link.external ? { target: '_blank', rel: 'noopener noreferrer' } : {}}
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom -->
|
||||
<div class="mt-12 border-t border-base-300 pt-8 text-center text-sm text-base-content/60">
|
||||
<p>
|
||||
© {currentYear} CSGOW.TF Team. Licensed under
|
||||
<a href="/license" class="hover:text-primary">GPL-3.0</a>
|
||||
</p>
|
||||
<p class="mt-2">
|
||||
Made with <Heart class="inline h-4 w-4 text-error" /> by the community, for the community.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
77
src/lib/components/layout/Header.svelte
Normal file
77
src/lib/components/layout/Header.svelte
Normal file
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { Search, Menu, X } from 'lucide-svelte';
|
||||
|
||||
let mobileMenuOpen = $state(false);
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Home', href: '/' },
|
||||
{ name: 'Matches', href: '/matches' },
|
||||
{ name: 'Players', href: '/players' },
|
||||
{ name: 'About', href: '/about' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<header class="sticky top-0 z-50 w-full border-b border-base-300 bg-base-100/95 backdrop-blur-md">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex h-16 items-center justify-between">
|
||||
<!-- Logo -->
|
||||
<a
|
||||
href="/"
|
||||
class="flex items-center gap-2 text-2xl font-bold transition-transform hover:scale-105"
|
||||
>
|
||||
<span class="text-primary">CS2</span><span class="text-secondary">.WTF</span>
|
||||
</a>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<nav class="hidden items-center gap-6 md:flex">
|
||||
{#each navigation as item}
|
||||
<a
|
||||
href={item.href}
|
||||
class="text-sm font-medium text-base-content/70 transition-colors hover:text-primary"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<!-- Search & Actions -->
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm hidden md:inline-flex"
|
||||
aria-label="Search"
|
||||
title="Search (Cmd+K)"
|
||||
>
|
||||
<Search class="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<!-- Mobile Menu Toggle -->
|
||||
<button
|
||||
class="btn btn-ghost btn-sm md:hidden"
|
||||
onclick={() => (mobileMenuOpen = !mobileMenuOpen)}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
{#if mobileMenuOpen}
|
||||
<X class="h-5 w-5" />
|
||||
{:else}
|
||||
<Menu class="h-5 w-5" />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Navigation -->
|
||||
{#if mobileMenuOpen}
|
||||
<nav class="animate-fade-in border-t border-base-300 py-4 md:hidden">
|
||||
{#each navigation as item}
|
||||
<a
|
||||
href={item.href}
|
||||
class="block px-4 py-2 text-sm font-medium text-base-content transition-colors hover:bg-base-200"
|
||||
onclick={() => (mobileMenuOpen = false)}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
37
src/lib/components/ui/Badge.svelte
Normal file
37
src/lib/components/ui/Badge.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
variant?: 'default' | 't-side' | 'ct-side' | 'success' | 'warning' | 'error' | 'info';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
class?: string;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { variant = 'default', size = 'md', class: className = '', children }: Props = $props();
|
||||
|
||||
const baseClasses = 'inline-flex items-center justify-center font-medium border rounded';
|
||||
|
||||
const variantClasses = {
|
||||
default: 'bg-base-300/50 border-base-content/20 text-base-content',
|
||||
't-side':
|
||||
'bg-terrorist/10 border-terrorist/30 text-terrorist-light backdrop-blur-sm font-semibold',
|
||||
'ct-side': 'bg-ct/10 border-ct/30 text-ct-light backdrop-blur-sm font-semibold',
|
||||
success: 'bg-success/10 border-success/30 text-success',
|
||||
warning: 'bg-warning/10 border-warning/30 text-warning',
|
||||
error: 'bg-error/10 border-error/30 text-error',
|
||||
info: 'bg-info/10 border-info/30 text-info'
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'px-2 py-0.5 text-xs',
|
||||
md: 'px-2.5 py-1 text-sm',
|
||||
lg: 'px-3 py-1.5 text-base'
|
||||
};
|
||||
|
||||
const classes = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`;
|
||||
</script>
|
||||
|
||||
<span class={classes}>
|
||||
{@render children()}
|
||||
</span>
|
||||
56
src/lib/components/ui/Button.svelte
Normal file
56
src/lib/components/ui/Button.svelte
Normal file
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
href?: string;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
disabled?: boolean;
|
||||
class?: string;
|
||||
onclick?: () => void;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
href,
|
||||
type = 'button',
|
||||
disabled = false,
|
||||
class: className = '',
|
||||
onclick,
|
||||
children
|
||||
}: Props = $props();
|
||||
|
||||
const baseClasses =
|
||||
'inline-flex items-center justify-center font-semibold transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-base-100 disabled:opacity-50 disabled:cursor-not-allowed';
|
||||
|
||||
const variantClasses = {
|
||||
primary:
|
||||
'bg-primary text-white hover:bg-primary-focus focus:ring-primary shadow-sm hover:shadow-lg hover:shadow-primary/30',
|
||||
secondary:
|
||||
'bg-secondary text-white hover:bg-secondary-focus focus:ring-secondary shadow-sm hover:shadow-lg hover:shadow-secondary/30',
|
||||
ghost:
|
||||
'bg-transparent border border-base-300 text-base-content hover:bg-base-300 hover:border-primary focus:ring-primary',
|
||||
danger: 'bg-error text-white hover:bg-error/90 focus:ring-error shadow-sm hover:shadow-lg'
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'px-3 py-1.5 text-sm rounded',
|
||||
md: 'px-4 py-2 text-base rounded-md',
|
||||
lg: 'px-6 py-3 text-lg rounded-lg'
|
||||
};
|
||||
|
||||
const classes = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`;
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a {href} class={classes} aria-disabled={disabled}>
|
||||
{@render children()}
|
||||
</a>
|
||||
{:else}
|
||||
<button {type} {disabled} {onclick} class={classes}>
|
||||
{@render children()}
|
||||
</button>
|
||||
{/if}
|
||||
49
src/lib/components/ui/Card.svelte
Normal file
49
src/lib/components/ui/Card.svelte
Normal file
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
variant?: 'default' | 'elevated' | 'interactive';
|
||||
padding?: 'none' | 'sm' | 'md' | 'lg';
|
||||
class?: string;
|
||||
onclick?: () => void;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
variant = 'default',
|
||||
padding = 'md',
|
||||
class: className = '',
|
||||
onclick,
|
||||
children
|
||||
}: Props = $props();
|
||||
|
||||
const baseClasses = 'bg-base-200 border border-base-300 rounded-md transition-all duration-200';
|
||||
|
||||
const variantClasses = {
|
||||
default: 'shadow-sm',
|
||||
elevated: 'shadow-lg shadow-black/10',
|
||||
interactive:
|
||||
'cursor-pointer hover:border-primary hover:shadow-lg hover:shadow-primary/20 hover:-translate-y-0.5'
|
||||
};
|
||||
|
||||
const paddingClasses = {
|
||||
none: '',
|
||||
sm: 'p-3',
|
||||
md: 'p-4',
|
||||
lg: 'p-6'
|
||||
};
|
||||
|
||||
const classes =
|
||||
`${baseClasses} ${variantClasses[variant]} ${paddingClasses[padding]} ${className}` +
|
||||
(onclick ? ' cursor-pointer' : '');
|
||||
</script>
|
||||
|
||||
{#if onclick}
|
||||
<button class={classes} {onclick}>
|
||||
{@render children()}
|
||||
</button>
|
||||
{:else}
|
||||
<div class={classes}>
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user