Initial setup of frontend and backend

This commit is contained in:
Julian Tabel
2026-02-04 17:13:58 +01:00
parent 259c200d93
commit 6ee53a0533
72 changed files with 5687 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
import { Link, Outlet } from 'react-router-dom'
export function Layout() {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<nav className="bg-white dark:bg-gray-800 shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<Link to="/" className="text-xl font-bold">
Nuzlocke Tracker
</Link>
</div>
<div className="flex items-center space-x-4">
<Link
to="/games"
className="px-3 py-2 rounded-md text-sm font-medium hover:bg-gray-100 dark:hover:bg-gray-700"
>
Games
</Link>
<Link
to="/rules"
className="px-3 py-2 rounded-md text-sm font-medium hover:bg-gray-100 dark:hover:bg-gray-700"
>
Rules
</Link>
<Link
to="/dashboard"
className="px-3 py-2 rounded-md text-sm font-medium hover:bg-gray-100 dark:hover:bg-gray-700"
>
Dashboard
</Link>
<Link
to="/encounters"
className="px-3 py-2 rounded-md text-sm font-medium hover:bg-gray-100 dark:hover:bg-gray-700"
>
Encounters
</Link>
</div>
</div>
</div>
</nav>
<main>
<Outlet />
</main>
</div>
)
}

View File

@@ -0,0 +1,71 @@
import { useState } from 'react'
interface RuleToggleProps {
name: string
description: string
enabled: boolean
onChange: (enabled: boolean) => void
}
export function RuleToggle({
name,
description,
enabled,
onChange,
}: RuleToggleProps) {
const [showTooltip, setShowTooltip] = useState(false)
return (
<div className="flex items-center justify-between py-3 border-b border-gray-200 dark:border-gray-700 last:border-0">
<div className="flex-1 pr-4">
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900 dark:text-gray-100">
{name}
</span>
<button
type="button"
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
onClick={() => setShowTooltip(!showTooltip)}
aria-label={`Info about ${name}`}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</button>
</div>
{showTooltip && (
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
{description}
</p>
)}
</div>
<button
type="button"
role="switch"
aria-checked={enabled}
onClick={() => onChange(!enabled)}
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
enabled ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-600'
}`}
>
<span
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
enabled ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
</div>
)
}

View File

@@ -0,0 +1,98 @@
import type { NuzlockeRules } from '../types/rules'
import { RULE_DEFINITIONS, DEFAULT_RULES } from '../types/rules'
import { RuleToggle } from './RuleToggle'
interface RulesConfigurationProps {
rules: NuzlockeRules
onChange: (rules: NuzlockeRules) => void
onReset?: () => void
}
export function RulesConfiguration({
rules,
onChange,
onReset,
}: RulesConfigurationProps) {
const coreRules = RULE_DEFINITIONS.filter((r) => r.category === 'core')
const difficultyRules = RULE_DEFINITIONS.filter(
(r) => r.category === 'difficulty'
)
const handleRuleChange = (key: keyof NuzlockeRules, value: boolean) => {
onChange({ ...rules, [key]: value })
}
const handleResetToDefault = () => {
onChange(DEFAULT_RULES)
onReset?.()
}
const enabledCount = Object.values(rules).filter(Boolean).length
const totalCount = Object.keys(rules).length
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
Rules Configuration
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
{enabledCount} of {totalCount} rules enabled
</p>
</div>
<button
type="button"
onClick={handleResetToDefault}
className="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
Reset to Default
</button>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
Core Rules
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
The fundamental rules of a Nuzlocke challenge
</p>
</div>
<div className="px-4">
{coreRules.map((rule) => (
<RuleToggle
key={rule.key}
name={rule.name}
description={rule.description}
enabled={rules[rule.key]}
onChange={(value) => handleRuleChange(rule.key, value)}
/>
))}
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
Difficulty Modifiers
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
Optional rules to increase the challenge
</p>
</div>
<div className="px-4">
{difficultyRules.map((rule) => (
<RuleToggle
key={rule.key}
name={rule.name}
description={rule.description}
enabled={rules[rule.key]}
onChange={(value) => handleRuleChange(rule.key, value)}
/>
))}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export { Layout } from './Layout'
export { RuleToggle } from './RuleToggle'
export { RulesConfiguration } from './RulesConfiguration'