WIP
This commit is contained in:
parent
74f511c447
commit
ab2caad554
|
@ -1,9 +1,10 @@
|
||||||
import { JSX } from 'preact/jsx-runtime'
|
import { JSX } from 'preact/jsx-runtime'
|
||||||
|
import { Icons } from './icons.tsx'
|
||||||
|
|
||||||
interface IconButtonProps {
|
interface IconButtonProps {
|
||||||
active?: boolean
|
active?: boolean
|
||||||
text?: string
|
text?: string
|
||||||
icon?: JSX.Element
|
icon?: keyof (typeof Icons)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IconButton(
|
export function IconButton(
|
||||||
|
@ -11,13 +12,22 @@ export function IconButton(
|
||||||
& IconButtonProps
|
& IconButtonProps
|
||||||
& JSX.HTMLAttributes<HTMLButtonElement>,
|
& JSX.HTMLAttributes<HTMLButtonElement>,
|
||||||
) {
|
) {
|
||||||
|
const Icon = typeof icon === 'string'
|
||||||
|
? Icons[icon as keyof typeof Icons]
|
||||||
|
: (() => <></>)
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
class={'p-2 sm:px-4 flex flex-col sm:flex-row flex-1 sm:flex-none bg-mantle justify-center items-center sm:justify-start text-center sm:text-left rounded' +
|
class={'p-2 sm:px-4 flex flex-col sm:flex-row flex-1 sm:flex-none bg-mantle justify-center items-center sm:justify-start text-center sm:text-left rounded' +
|
||||||
(active ? ' text-primary' : '')}
|
(active ? ' text-primary' : '')}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div class='sm:mr-2'>{icon}</div>
|
{icon !== undefined
|
||||||
|
? (
|
||||||
|
<div class='sm:mr-2'>
|
||||||
|
<Icon />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: ''}
|
||||||
<div class=''>{children}</div>
|
<div class=''>{children}</div>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
34
components/IconLink.tsx
Normal file
34
components/IconLink.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { JSX } from 'preact/jsx-runtime'
|
||||||
|
import { Icons } from './icons.tsx'
|
||||||
|
|
||||||
|
interface IconLinkProps {
|
||||||
|
active?: boolean
|
||||||
|
text?: string
|
||||||
|
icon?: keyof (typeof Icons)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconLink(
|
||||||
|
{ icon, children, active, ...props }:
|
||||||
|
& IconLinkProps
|
||||||
|
& JSX.HTMLAttributes<HTMLAnchorElement>,
|
||||||
|
) {
|
||||||
|
const Icon = typeof icon === 'string'
|
||||||
|
? Icons[icon as keyof typeof Icons]
|
||||||
|
: (() => <></>)
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
class={'p-2 sm:px-4 flex flex-col sm:flex-row flex-1 sm:flex-none bg-mantle justify-center items-center sm:justify-start text-center sm:text-left rounded hover:bg-crust transition-colors' +
|
||||||
|
(active ? ' text-primary' : '')}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{icon !== undefined
|
||||||
|
? (
|
||||||
|
<span class='sm:mr-2 block'>
|
||||||
|
<Icon />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
: ''}
|
||||||
|
<div class=''>{children}</div>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
22
components/icons.tsx
Normal file
22
components/icons.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
export const Icons = {
|
||||||
|
'SomeIcon': SomeIcon,
|
||||||
|
}
|
||||||
|
|
||||||
|
function SomeIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
fill='none'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke='currentColor'
|
||||||
|
className='size-6'
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
d='M4.26 10.147a60.438 60.438 0 0 0-.491 6.347A48.62 48.62 0 0 1 12 20.904a48.62 48.62 0 0 1 8.232-4.41 60.46 60.46 0 0 0-.491-6.347m-15.482 0a50.636 50.636 0 0 0-2.658-.813A59.906 59.906 0 0 1 12 3.493a59.903 59.903 0 0 1 10.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.717 50.717 0 0 1 12 13.489a50.702 50.702 0 0 1 7.74-3.342M6.75 15a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm0 0v-3.675A55.378 55.378 0 0 1 12 8.443m-7.007 11.55A5.981 5.981 0 0 0 6.75 15.75v-1.5'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
|
@ -25,6 +25,7 @@
|
||||||
"preact": "https://esm.sh/preact@10.19.6",
|
"preact": "https://esm.sh/preact@10.19.6",
|
||||||
"preact/": "https://esm.sh/preact@10.19.6/",
|
"preact/": "https://esm.sh/preact@10.19.6/",
|
||||||
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
|
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
|
||||||
|
"preact/hooks": "https://esm.sh/preact@10.19.6/hooks",
|
||||||
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
|
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
|
||||||
"tailwindcss": "npm:tailwindcss@3.4.1",
|
"tailwindcss": "npm:tailwindcss@3.4.1",
|
||||||
"tailwindcss/": "npm:/tailwindcss@3.4.1/",
|
"tailwindcss/": "npm:/tailwindcss@3.4.1/",
|
||||||
|
|
|
@ -5,11 +5,13 @@
|
||||||
import * as $_404 from './routes/_404.tsx'
|
import * as $_404 from './routes/_404.tsx'
|
||||||
import * as $_app from './routes/_app.tsx'
|
import * as $_app from './routes/_app.tsx'
|
||||||
import * as $api_joke from './routes/api/joke.ts'
|
import * as $api_joke from './routes/api/joke.ts'
|
||||||
|
import * as $bank_subpage_index from './routes/bank/[[subpage]]/index.tsx'
|
||||||
import * as $greet_name_ from './routes/greet/[name].tsx'
|
import * as $greet_name_ from './routes/greet/[name].tsx'
|
||||||
import * as $index from './routes/index.tsx'
|
import * as $index from './routes/index.tsx'
|
||||||
import * as $BudgetCard from './islands/BudgetCard.tsx'
|
import * as $BudgetCard from './islands/BudgetCard.tsx'
|
||||||
import * as $Counter from './islands/Counter.tsx'
|
import * as $Counter from './islands/Counter.tsx'
|
||||||
import * as $Dashboard from './islands/Dashboard.tsx'
|
import * as $Dashboard from './islands/Dashboard.tsx'
|
||||||
|
import * as $Nav from './islands/Nav.tsx'
|
||||||
import { type Manifest } from '$fresh/server.ts'
|
import { type Manifest } from '$fresh/server.ts'
|
||||||
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
|
@ -17,6 +19,7 @@ const manifest = {
|
||||||
'./routes/_404.tsx': $_404,
|
'./routes/_404.tsx': $_404,
|
||||||
'./routes/_app.tsx': $_app,
|
'./routes/_app.tsx': $_app,
|
||||||
'./routes/api/joke.ts': $api_joke,
|
'./routes/api/joke.ts': $api_joke,
|
||||||
|
'./routes/bank/[[subpage]]/index.tsx': $bank_subpage_index,
|
||||||
'./routes/greet/[name].tsx': $greet_name_,
|
'./routes/greet/[name].tsx': $greet_name_,
|
||||||
'./routes/index.tsx': $index,
|
'./routes/index.tsx': $index,
|
||||||
},
|
},
|
||||||
|
@ -24,6 +27,7 @@ const manifest = {
|
||||||
'./islands/BudgetCard.tsx': $BudgetCard,
|
'./islands/BudgetCard.tsx': $BudgetCard,
|
||||||
'./islands/Counter.tsx': $Counter,
|
'./islands/Counter.tsx': $Counter,
|
||||||
'./islands/Dashboard.tsx': $Dashboard,
|
'./islands/Dashboard.tsx': $Dashboard,
|
||||||
|
'./islands/Nav.tsx': $Nav,
|
||||||
},
|
},
|
||||||
baseUrl: import.meta.url,
|
baseUrl: import.meta.url,
|
||||||
} satisfies Manifest
|
} satisfies Manifest
|
||||||
|
|
|
@ -119,7 +119,6 @@ export function ProgressBarCard(
|
||||||
& ProgressBarCardProps
|
& ProgressBarCardProps
|
||||||
& JSX.HTMLAttributes<HTMLDivElement>,
|
& JSX.HTMLAttributes<HTMLDivElement>,
|
||||||
) {
|
) {
|
||||||
console.log({ fillRatio })
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class='bg-surface0 rounded shadow hover:bg-surface1 transition-colors flex justify-between relative z-0 cursor-pointer'
|
class='bg-surface0 rounded shadow hover:bg-surface1 transition-colors flex justify-between relative z-0 cursor-pointer'
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { useSignal } from 'https://esm.sh/v135/@preact/signals@1.2.2/X-ZS8q/dist/signals.js'
|
|
||||||
import { Budget } from '../models.ts'
|
import { Budget } from '../models.ts'
|
||||||
import { BudgetCard, ProgressBarCard } from './BudgetCard.tsx'
|
import { BudgetCard, ProgressBarCard } from './BudgetCard.tsx'
|
||||||
import { IconButton } from '../components/IconButton.tsx'
|
|
||||||
import { Currency } from '../components/Currency.tsx'
|
import { Currency } from '../components/Currency.tsx'
|
||||||
|
import { Nav } from '../islands/Nav.tsx'
|
||||||
|
import { IS_BROWSER } from '$fresh/runtime.ts'
|
||||||
|
import { useCallback, useEffect } from 'preact/hooks'
|
||||||
|
import { useSignal } from '@preact/signals'
|
||||||
|
|
||||||
export default function Dashboard() {
|
|
||||||
const activeNavItemIndex = useSignal(0)
|
|
||||||
const budgets: Budget[] = [
|
const budgets: Budget[] = [
|
||||||
{
|
{
|
||||||
name: 'Groceries',
|
name: 'Groceries',
|
||||||
|
@ -34,65 +34,26 @@ export default function Dashboard() {
|
||||||
spent: 1310.47,
|
spent: 1310.47,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
const navItems = [
|
|
||||||
{
|
export function BudgetsOverview() {
|
||||||
text: 'Some Text',
|
let sig = useSignal(IS_BROWSER ? globalThis.location.hash : '')
|
||||||
icon: (
|
|
||||||
<svg
|
addEventListener('popstate', (ev) => {
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
console.log('popstate', ev)
|
||||||
fill='none'
|
sig.value = globalThis.location.hash
|
||||||
viewBox='0 0 24 24'
|
ev.preventDefault()
|
||||||
strokeWidth={1.5}
|
return false
|
||||||
stroke='currentColor'
|
})
|
||||||
className='size-6'
|
|
||||||
>
|
useEffect(() => {
|
||||||
<path
|
setTimeout(() => {
|
||||||
strokeLinecap='round'
|
console.log('Timeout!')
|
||||||
strokeLinejoin='round'
|
}, 2000)
|
||||||
d='M4.26 10.147a60.438 60.438 0 0 0-.491 6.347A48.62 48.62 0 0 1 12 20.904a48.62 48.62 0 0 1 8.232-4.41 60.46 60.46 0 0 0-.491-6.347m-15.482 0a50.636 50.636 0 0 0-2.658-.813A59.906 59.906 0 0 1 12 3.493a59.903 59.903 0 0 1 10.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.717 50.717 0 0 1 12 13.489a50.702 50.702 0 0 1 7.74-3.342M6.75 15a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm0 0v-3.675A55.378 55.378 0 0 1 12 8.443m-7.007 11.55A5.981 5.981 0 0 0 6.75 15.75v-1.5'
|
addEventListener('popstate', (ev) => {
|
||||||
/>
|
console.log('popstate', ev)
|
||||||
</svg>
|
sig.value = globalThis.location.hash
|
||||||
),
|
})
|
||||||
},
|
}, [sig])
|
||||||
{
|
|
||||||
text: 'Some Text',
|
|
||||||
icon: (
|
|
||||||
<svg
|
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
|
||||||
fill='none'
|
|
||||||
viewBox='0 0 24 24'
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke='currentColor'
|
|
||||||
className='size-6'
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap='round'
|
|
||||||
strokeLinejoin='round'
|
|
||||||
d='m20.25 7.5-.625 10.632a2.25 2.25 0 0 1-2.247 2.118H6.622a2.25 2.25 0 0 1-2.247-2.118L3.75 7.5m8.25 3v6.75m0 0-3-3m3 3 3-3M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125Z'
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Some Text',
|
|
||||||
icon: (
|
|
||||||
<svg
|
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
|
||||||
fill='none'
|
|
||||||
viewBox='0 0 24 24'
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke='currentColor'
|
|
||||||
className='size-6'
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap='round'
|
|
||||||
strokeLinejoin='round'
|
|
||||||
d='M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25'
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const uncategorizedSpend = 851.22
|
const uncategorizedSpend = 851.22
|
||||||
|
|
||||||
|
@ -100,9 +61,10 @@ export default function Dashboard() {
|
||||||
<div class='h-[100vh] flex flex-col max-w-full'>
|
<div class='h-[100vh] flex flex-col max-w-full'>
|
||||||
<header class='bg-mantle'>
|
<header class='bg-mantle'>
|
||||||
<h1 class='text-2xl font-mono p-2 px-2'>FlanBank</h1>
|
<h1 class='text-2xl font-mono p-2 px-2'>FlanBank</h1>
|
||||||
|
{sig.value}
|
||||||
</header>
|
</header>
|
||||||
<section class='grid grid-rows-[1fr_auto] sm:grid-cols-2 sm:grid-rows-1 sm:grid-cols-[1fr_auto] flex-1 max-w-full max-w-2xl mx-auto'>
|
<section class='grid grid-rows-[1fr_auto] sm:grid-cols-2 sm:grid-rows-1 sm:grid-cols-[1fr_auto] flex-1 max-w-full max-w-2xl mx-auto'>
|
||||||
<main class='p-2 sm:pr-0 flex flex-col gap-2 max-w-full'>
|
<main class='p-2 sm:pr-0 flex flex-col gap-2 max-w-full' f-client-nav>
|
||||||
{/*<h1>Budgets Overview</h1>*/}
|
{/*<h1>Budgets Overview</h1>*/}
|
||||||
<section class='flex flex-col gap-2 max-w-full'>
|
<section class='flex flex-col gap-2 max-w-full'>
|
||||||
{uncategorizedSpend <= 0 ? '' : (
|
{uncategorizedSpend <= 0 ? '' : (
|
||||||
|
@ -123,21 +85,7 @@ export default function Dashboard() {
|
||||||
{budgets.map((budget, _i) => <BudgetCard budget={budget} />)}
|
{budgets.map((budget, _i) => <BudgetCard budget={budget} />)}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
<Nav />
|
||||||
<nav class='w-full flex flex-1 sm:flex-col sm:bg-bg gap-2 p-2'>
|
|
||||||
{navItems.map(({ text, ...props }, i) => (
|
|
||||||
<IconButton
|
|
||||||
{...props}
|
|
||||||
active={i == activeNavItemIndex.value}
|
|
||||||
onClick={(ev) => {
|
|
||||||
console.log({ ev, i })
|
|
||||||
activeNavItemIndex.value = i
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</IconButton>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
47
islands/Nav.tsx
Normal file
47
islands/Nav.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { JSX } from 'preact/jsx-runtime'
|
||||||
|
import { IconLink } from '../components/IconLink.tsx'
|
||||||
|
import { Icons } from '../components/icons.tsx'
|
||||||
|
import { IS_BROWSER } from '$fresh/runtime.ts'
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
text: string
|
||||||
|
href: string
|
||||||
|
icon?: keyof typeof Icons
|
||||||
|
}
|
||||||
|
|
||||||
|
const navItems: NavItem[] = [
|
||||||
|
{
|
||||||
|
text: 'Budgets',
|
||||||
|
href: '/bank/budgets',
|
||||||
|
icon: 'SomeIcon',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Transactions',
|
||||||
|
href: '/bank/transactions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Something',
|
||||||
|
href: '/bank/something',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function Nav(_props: JSX.HTMLAttributes<HTMLElement>) {
|
||||||
|
// TODO: on nav, the `active` field is not updated
|
||||||
|
globalThis.addEventListener('popstate', (ev) => {
|
||||||
|
console.log('popstate', ev)
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<nav class='w-full flex flex-1 sm:flex-col sm:bg-bg gap-2 p-2'>
|
||||||
|
{navItems.map(({ text, ...props }, _i) => (
|
||||||
|
<IconLink
|
||||||
|
{...props}
|
||||||
|
icon={props.icon}
|
||||||
|
active={IS_BROWSER &&
|
||||||
|
globalThis.location.toString().endsWith(props.href)}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</IconLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import { type PageProps } from '$fresh/server.ts'
|
import { type PageProps } from '$fresh/server.ts'
|
||||||
|
import { Partial } from '$fresh/runtime.ts'
|
||||||
export default function App({ Component }: PageProps) {
|
export default function App({ Component }: PageProps) {
|
||||||
return (
|
return (
|
||||||
<html>
|
<html>
|
||||||
|
@ -8,8 +9,13 @@ export default function App({ Component }: PageProps) {
|
||||||
<title>Bank</title>
|
<title>Bank</title>
|
||||||
<link rel='stylesheet' href='/styles.css' />
|
<link rel='stylesheet' href='/styles.css' />
|
||||||
</head>
|
</head>
|
||||||
<body class='bg-bg text-text w-screen min-w-full min-h-dvh overflow-hidden'>
|
<body
|
||||||
|
class='bg-bg text-text w-screen min-w-full min-h-dvh overflow-hidden'
|
||||||
|
f-client-nav
|
||||||
|
>
|
||||||
|
<Partial name='body'>
|
||||||
<Component />
|
<Component />
|
||||||
|
</Partial>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|
21
routes/bank/[[subpage]]/index.tsx
Normal file
21
routes/bank/[[subpage]]/index.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { PageProps } from '$fresh/server.ts'
|
||||||
|
import { BudgetsOverview } from '../../../islands/Dashboard.tsx'
|
||||||
|
|
||||||
|
export default function Bank(props: PageProps) {
|
||||||
|
const { subpage } = props.params
|
||||||
|
let Component = () => <BudgetsOverview/>;
|
||||||
|
switch (subpage) {
|
||||||
|
case 'transactions':
|
||||||
|
Component = () => <>Transactions</>
|
||||||
|
break
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class='h-[100vh] flex flex-col max-w-full'>
|
||||||
|
<header class='bg-mantle'>
|
||||||
|
<h1 class='text-2xl font-mono p-2 px-2'>FlanBank</h1>
|
||||||
|
{sig.value}
|
||||||
|
</header>
|
||||||
|
<section class='grid grid-rows-[1fr_auto] sm:grid-cols-2 sm:grid-rows-1 sm:grid-cols-[1fr_auto] flex-1 max-w-full max-w-2xl mx-auto'>
|
||||||
|
<main class='p-2 sm:pr-0 flex flex-col gap-2 max-w-full'>}
|
|
@ -1,5 +1,6 @@
|
||||||
import Dashboard from '../islands/Dashboard.tsx'
|
export function handler(_req: Request): Response {
|
||||||
|
return new Response('', {
|
||||||
export default function Home() {
|
status: 307,
|
||||||
return <Dashboard />
|
headers: { Location: '/bank' },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue