Initial setup of frontend and backend
This commit is contained in:
48
frontend/src/components/Layout.tsx
Normal file
48
frontend/src/components/Layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
71
frontend/src/components/RuleToggle.tsx
Normal file
71
frontend/src/components/RuleToggle.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
98
frontend/src/components/RulesConfiguration.tsx
Normal file
98
frontend/src/components/RulesConfiguration.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
3
frontend/src/components/index.ts
Normal file
3
frontend/src/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { Layout } from './Layout'
|
||||
export { RuleToggle } from './RuleToggle'
|
||||
export { RulesConfiguration } from './RulesConfiguration'
|
||||
Reference in New Issue
Block a user