Skip to content
Merged
37 changes: 2 additions & 35 deletions apps/frontend/src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,9 @@ export default function Footer() {
const year = new Date().getFullYear()

return (
<footer className="mt-20 border-t border-[var(--line)] px-4 pb-14 pt-10 text-[var(--sea-ink-soft)]">
<footer className="mt-20 px-4 pb-14 pt-10 text-[var(--cream-dark)]">
<div className="page-wrap flex flex-col items-center justify-between gap-4 text-center sm:flex-row sm:text-left">
<p className="m-0 text-sm">
&copy; {year} Your name here. All rights reserved.
</p>
<p className="island-kicker m-0">Built with TanStack Start</p>
</div>
<div className="mt-4 flex justify-center gap-4">
<a
href="https://x.com/tan_stack"
target="_blank"
rel="noreferrer"
className="rounded-xl p-2 text-[var(--sea-ink-soft)] transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)]"
>
<span className="sr-only">Follow TanStack on X</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32">
<path
fill="currentColor"
d="M12.6 1h2.2L10 6.48 15.64 15h-4.41L7.78 9.82 3.23 15H1l5.14-5.84L.72 1h4.52l3.12 4.73L12.6 1zm-.77 12.67h1.22L4.57 2.26H3.26l8.57 11.41z"
/>
</svg>
</a>
<a
href="https://github.com/TanStack"
target="_blank"
rel="noreferrer"
className="rounded-xl p-2 text-[var(--sea-ink-soft)] transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)]"
>
<span className="sr-only">Go to TanStack GitHub</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32">
<path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
/>
</svg>
</a>
<p className="m-0 text-sm">&copy; {year} RSM. All rights reserved.</p>
</div>
</footer>
)
Expand Down
97 changes: 9 additions & 88 deletions apps/frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,96 +1,17 @@
import { Link } from '@tanstack/react-router'
import ThemeToggle from './ThemeToggle'

export default function Header() {
return (
<header className="sticky top-0 z-50 border-b border-[var(--line)] bg-[var(--header-bg)] px-4 backdrop-blur-lg">
<nav className="page-wrap flex flex-wrap items-center gap-x-3 gap-y-2 py-3 sm:py-4">
<h2 className="m-0 flex-shrink-0 text-base font-semibold tracking-tight">
<Link
to="/"
className="inline-flex items-center gap-2 rounded-full border border-[var(--chip-line)] bg-[var(--chip-bg)] px-3 py-1.5 text-sm text-[var(--sea-ink)] no-underline shadow-[0_8px_24px_rgba(30,90,72,0.08)] sm:px-4 sm:py-2"
>
<span className="h-2 w-2 rounded-full bg-[linear-gradient(90deg,#56c6be,#7ed3bf)]" />
TanStack Start
</Link>
<header className="sticky top-0 z-50 border-b border-[var(--harp-gold)] bg-[var(--cream-head)] px-4 backdrop-blur-lg">
<nav className="page-wrap flex flex-wrap items-center justify-between gap-x-3 gap-y-2 py-3 sm:py-4">
<h2 className="m-0 w-full flex-1 text-base font-semibold">
<div className="flex w-full items-center justify-between gap-3">
<p className="m-0 p-0 text-left text-[var(--cream-dark)]">
G-SPOTTER
</p>
<ThemeToggle />
</div>
</h2>

<div className="order-3 flex w-full flex-wrap items-center gap-x-4 gap-y-1 pb-1 text-sm font-semibold sm:order-none sm:w-auto sm:flex-nowrap sm:pb-0">
<Link
to="/"
className="nav-link"
activeProps={{ className: 'nav-link is-active' }}
>
Home
</Link>
<Link
to="/about"
className="nav-link"
activeProps={{ className: 'nav-link is-active' }}
>
About
</Link>
<a
href="https://tanstack.com/start/latest/docs/framework/react/overview"
className="nav-link"
target="_blank"
rel="noreferrer"
>
Docs
</a>
<details className="relative w-full sm:w-auto">
<summary className="nav-link list-none cursor-pointer">
Demos
</summary>
<div className="mt-2 min-w-56 rounded-xl border border-[var(--line)] bg-[var(--header-bg)] p-2 shadow-lg sm:absolute sm:right-0">
<a
href="/demo/todos"
className="block rounded-lg px-3 py-2 text-sm text-[var(--sea-ink-soft)] no-underline transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)]"
>
Todos
</a>
<a
href="/demo/tanstack-query"
className="block rounded-lg px-3 py-2 text-sm text-[var(--sea-ink-soft)] no-underline transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)]"
>
TanStack Query
</a>
</div>
</details>
</div>

<div className="ml-auto flex items-center gap-1.5 sm:gap-2">
<a
href="https://x.com/tan_stack"
target="_blank"
rel="noreferrer"
className="hidden rounded-xl p-2 text-[var(--sea-ink-soft)] transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)] sm:block"
>
<span className="sr-only">Follow TanStack on X</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="24" height="24">
<path
fill="currentColor"
d="M12.6 1h2.2L10 6.48 15.64 15h-4.41L7.78 9.82 3.23 15H1l5.14-5.84L.72 1h4.52l3.12 4.73L12.6 1zm-.77 12.67h1.22L4.57 2.26H3.26l8.57 11.41z"
/>
</svg>
</a>
<a
href="https://github.com/TanStack"
target="_blank"
rel="noreferrer"
className="hidden rounded-xl p-2 text-[var(--sea-ink-soft)] transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)] sm:block"
>
<span className="sr-only">Go to TanStack GitHub</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="24" height="24">
<path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
/>
</svg>
</a>

<ThemeToggle />
</div>
</nav>
</header>
)
Expand Down
51 changes: 12 additions & 39 deletions apps/frontend/src/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,81 +1,54 @@
import { useEffect, useState } from 'react'

type ThemeMode = 'light' | 'dark' | 'auto'
type ThemeMode = 'light' | 'dark'

function getInitialMode(): ThemeMode {
if (typeof window === 'undefined') {
return 'auto'
return 'dark'
}

const stored = window.localStorage.getItem('theme')
if (stored === 'light' || stored === 'dark' || stored === 'auto') {
if (stored === 'light' || stored === 'dark') {
return stored
}

return 'auto'
return 'dark'
}

function applyThemeMode(mode: ThemeMode) {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
const resolved = mode === 'auto' ? (prefersDark ? 'dark' : 'light') : mode

document.documentElement.classList.remove('light', 'dark')
document.documentElement.classList.add(resolved)

if (mode === 'auto') {
document.documentElement.removeAttribute('data-theme')
} else {
document.documentElement.setAttribute('data-theme', mode)
}

document.documentElement.style.colorScheme = resolved
document.documentElement.classList.add(mode)
document.documentElement.setAttribute('data-theme', mode)
document.documentElement.style.colorScheme = mode
}

export default function ThemeToggle() {
const [mode, setMode] = useState<ThemeMode>('auto')
const [mode, setMode] = useState<ThemeMode>('dark')

useEffect(() => {
const initialMode = getInitialMode()
setMode(initialMode)
applyThemeMode(initialMode)
}, [])

useEffect(() => {
if (mode !== 'auto') {
return
}

const media = window.matchMedia('(prefers-color-scheme: dark)')
const onChange = () => applyThemeMode('auto')

media.addEventListener('change', onChange)
return () => {
media.removeEventListener('change', onChange)
}
}, [mode])

function toggleMode() {
const nextMode: ThemeMode =
mode === 'light' ? 'dark' : mode === 'dark' ? 'auto' : 'light'
const nextMode: ThemeMode = mode === 'dark' ? 'light' : 'dark'
setMode(nextMode)
applyThemeMode(nextMode)
window.localStorage.setItem('theme', nextMode)
}

const label =
mode === 'auto'
? 'Theme mode: auto (system). Click to switch to light mode.'
: `Theme mode: ${mode}. Click to switch mode.`
const label = `Theme mode: ${mode}. Click to switch to ${mode === 'dark' ? 'light' : 'dark'} mode.`

return (
<button
type="button"
onClick={toggleMode}
aria-label={label}
title={label}
className="rounded-full border border-[var(--chip-line)] bg-[var(--chip-bg)] px-3 py-1.5 text-sm font-semibold text-[var(--sea-ink)] shadow-[0_8px_22px_rgba(30,90,72,0.08)] transition hover:-translate-y-0.5"
className="rounded-full bg-[var(--cream-dark)] px-3 py-1.5 text-sm font-semibold text-[var(--stout-black)] shadow-[0_8px_22px_rgba(30,90,72,0.08)] transition hover:-translate-y-0.5"
>
{mode === 'auto' ? 'Auto' : mode === 'dark' ? 'Dark' : 'Light'}
{mode === 'dark' ? 'Dark' : 'Light'}
</button>
)
}
31 changes: 31 additions & 0 deletions apps/frontend/src/components/ViewVenue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { venue } from '#/types/venue'

export default function ViewVenue(Current: venue) {
const fmt = (iso: string) => iso.slice(11, 16)

return (
<article className="rounded-2xl border border-[var(--harp-gold)] bg-[var(--cream)] p-5 text-[var(--sea-ink)]">
<p className="text-xs font-bold tracking-widest uppercase text-[var(--)]">
{Current.city.name}
</p>
<h2 className="mt-1 text-xl font-extrabold">{Current.name}</h2>
<p className="mt-1 text-sm text-[var(--sea-ink-soft)]">
{Current.address}
</p>
<h3 className="mt-4 text-sm font-bold">Opening hours</h3>
<ul className="mt-1">
{Current.openingHours.map((oh, i) => (
<li
key={i}
className="flex items-baseline justify-between gap-4 border-t border-[var(--line)] py-2 text-sm"
>
<span className="font-semibold">{oh.days.join(', ')}</span>
<span className="text-[var(--sea-ink-soft)]">
{fmt(oh.opening)} – {fmt(oh.closing)}
</span>
</li>
))}
</ul>
</article>
)
}
40 changes: 39 additions & 1 deletion apps/frontend/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ import TanStackQueryDevtools from '../integrations/tanstack-query/devtools'
import appCss from '../styles.css?url'

import type { QueryClient } from '@tanstack/react-query'
import { useQuery } from '@tanstack/react-query'
import ViewVenue from '#/components/ViewVenue'
import { graphqlFetch } from '#/graphql/client'
import type { venue } from '#/types/venue'

interface MyRouterContext {
queryClient: QueryClient
}

const THEME_INIT_SCRIPT = `(function(){try{var stored=window.localStorage.getItem('theme');var mode=(stored==='light'||stored==='dark'||stored==='auto')?stored:'auto';var prefersDark=window.matchMedia('(prefers-color-scheme: dark)').matches;var resolved=mode==='auto'?(prefersDark?'dark':'light'):mode;var root=document.documentElement;root.classList.remove('light','dark');root.classList.add(resolved);if(mode==='auto'){root.removeAttribute('data-theme')}else{root.setAttribute('data-theme',mode)}root.style.colorScheme=resolved;}catch(e){}})();`
const THEME_INIT_SCRIPT = `(function(){try{var stored=window.localStorage.getItem('theme');var mode=(stored==='light'||stored==='dark')?stored:'dark';var root=document.documentElement;root.classList.remove('light','dark');root.classList.add(mode);root.setAttribute('data-theme',mode);root.style.colorScheme=mode;}catch(e){}})();`

export const Route = createRootRouteWithContext<MyRouterContext>()({
head: () => ({
Expand All @@ -44,6 +48,39 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
shellComponent: RootDocument,
})

const GET_VENUE = `
query GetVenues {
venue {
id
name
address
city {
id
name
}
openingHours {
days
opening
closing
}
}
}
`

function VenueLoader() {
const { data, isPending, isError } = useQuery({
queryKey: ['venues'],
queryFn: () => graphqlFetch<{ venue: venue[] }>(GET_VENUE),
})

if (isPending) return <p>Loading venue…</p>
if (isError) return <div>error</div>
// Hardcoded: show first venue for now
if (data.venue.length == 0) return <div>No venues found</div>
const first = data.venue[0]
return <ViewVenue {...first} />
}

function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
Expand All @@ -60,6 +97,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
<body className="font-sans antialiased [overflow-wrap:anywhere] selection:bg-[rgba(79,184,178,0.24)]">
<Header />
{children}
<VenueLoader />
<Footer />
<TanStackDevtools
config={{
Expand Down
Loading