This commit is contained in:
Daniel Flanagan 2024-08-25 11:21:06 -05:00
parent 74f511c447
commit ab2caad554
11 changed files with 209 additions and 116 deletions

View file

@ -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
View 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
View 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>
)
}

View file

@ -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/",

View file

@ -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

View file

@ -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'

View file

@ -1,98 +1,59 @@
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 budgets: Budget[] = [
const activeNavItemIndex = useSignal(0) {
const budgets: Budget[] = [ name: 'Groceries',
{ target: 1000,
name: 'Groceries', spent: 367.97,
target: 1000, },
spent: 367.97, {
}, name:
{ 'A very long budget name that should definitely be shortened to something reasonable',
name: target: 100000000,
'A very long budget name that should definitely be shortened to something reasonable', spent: 26893085.56,
target: 100000000, },
spent: 26893085.56, {
}, name: 'Fast Food',
{ target: 300,
name: 'Fast Food', spent: 420.69,
target: 300, },
spent: 420.69, {
}, name: 'Insurance',
{ target: 220.44,
name: 'Insurance', spent: 0,
target: 220.44, },
spent: 0, {
}, name: 'Mortgage',
{ target: 1310.47,
name: 'Mortgage', spent: 1310.47,
target: 1310.47, },
spent: 1310.47, ]
},
] export function BudgetsOverview() {
const navItems = [ let sig = useSignal(IS_BROWSER ? globalThis.location.hash : '')
{
text: 'Some Text', addEventListener('popstate', (ev) => {
icon: ( console.log('popstate', ev)
<svg sig.value = globalThis.location.hash
xmlns='http://www.w3.org/2000/svg' ev.preventDefault()
fill='none' return false
viewBox='0 0 24 24' })
strokeWidth={1.5}
stroke='currentColor' useEffect(() => {
className='size-6' setTimeout(() => {
> console.log('Timeout!')
<path }, 2000)
strokeLinecap='round' addEventListener('popstate', (ev) => {
strokeLinejoin='round' console.log('popstate', ev)
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' sig.value = globalThis.location.hash
/> })
</svg> }, [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
View 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>
)
}

View file

@ -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
<Component /> class='bg-bg text-text w-screen min-w-full min-h-dvh overflow-hidden'
f-client-nav
>
<Partial name='body'>
<Component />
</Partial>
</body> </body>
</html> </html>
) )

View 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'>}

View file

@ -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' },
})
} }