WIP
This commit is contained in:
parent
74f511c447
commit
ab2caad554
|
@ -1,9 +1,10 @@
|
|||
import { JSX } from 'preact/jsx-runtime'
|
||||
import { Icons } from './icons.tsx'
|
||||
|
||||
interface IconButtonProps {
|
||||
active?: boolean
|
||||
text?: string
|
||||
icon?: JSX.Element
|
||||
icon?: keyof (typeof Icons)
|
||||
}
|
||||
|
||||
export function IconButton(
|
||||
|
@ -11,13 +12,22 @@ export function IconButton(
|
|||
& IconButtonProps
|
||||
& JSX.HTMLAttributes<HTMLButtonElement>,
|
||||
) {
|
||||
const Icon = typeof icon === 'string'
|
||||
? Icons[icon as keyof typeof Icons]
|
||||
: (() => <></>)
|
||||
return (
|
||||
<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' +
|
||||
(active ? ' text-primary' : '')}
|
||||
{...props}
|
||||
>
|
||||
<div class='sm:mr-2'>{icon}</div>
|
||||
{icon !== undefined
|
||||
? (
|
||||
<div class='sm:mr-2'>
|
||||
<Icon />
|
||||
</div>
|
||||
)
|
||||
: ''}
|
||||
<div class=''>{children}</div>
|
||||
</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/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",
|
||||
"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 $_app from './routes/_app.tsx'
|
||||
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 $index from './routes/index.tsx'
|
||||
import * as $BudgetCard from './islands/BudgetCard.tsx'
|
||||
import * as $Counter from './islands/Counter.tsx'
|
||||
import * as $Dashboard from './islands/Dashboard.tsx'
|
||||
import * as $Nav from './islands/Nav.tsx'
|
||||
import { type Manifest } from '$fresh/server.ts'
|
||||
|
||||
const manifest = {
|
||||
|
@ -17,6 +19,7 @@ const manifest = {
|
|||
'./routes/_404.tsx': $_404,
|
||||
'./routes/_app.tsx': $_app,
|
||||
'./routes/api/joke.ts': $api_joke,
|
||||
'./routes/bank/[[subpage]]/index.tsx': $bank_subpage_index,
|
||||
'./routes/greet/[name].tsx': $greet_name_,
|
||||
'./routes/index.tsx': $index,
|
||||
},
|
||||
|
@ -24,6 +27,7 @@ const manifest = {
|
|||
'./islands/BudgetCard.tsx': $BudgetCard,
|
||||
'./islands/Counter.tsx': $Counter,
|
||||
'./islands/Dashboard.tsx': $Dashboard,
|
||||
'./islands/Nav.tsx': $Nav,
|
||||
},
|
||||
baseUrl: import.meta.url,
|
||||
} satisfies Manifest
|
||||
|
|
|
@ -119,7 +119,6 @@ export function ProgressBarCard(
|
|||
& ProgressBarCardProps
|
||||
& JSX.HTMLAttributes<HTMLDivElement>,
|
||||
) {
|
||||
console.log({ fillRatio })
|
||||
return (
|
||||
<div
|
||||
class='bg-surface0 rounded shadow hover:bg-surface1 transition-colors flex justify-between relative z-0 cursor-pointer'
|
||||
|
|
|
@ -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 { BudgetCard, ProgressBarCard } from './BudgetCard.tsx'
|
||||
import { IconButton } from '../components/IconButton.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[] = [
|
||||
{
|
||||
name: 'Groceries',
|
||||
target: 1000,
|
||||
spent: 367.97,
|
||||
},
|
||||
{
|
||||
name:
|
||||
'A very long budget name that should definitely be shortened to something reasonable',
|
||||
target: 100000000,
|
||||
spent: 26893085.56,
|
||||
},
|
||||
{
|
||||
name: 'Fast Food',
|
||||
target: 300,
|
||||
spent: 420.69,
|
||||
},
|
||||
{
|
||||
name: 'Insurance',
|
||||
target: 220.44,
|
||||
spent: 0,
|
||||
},
|
||||
{
|
||||
name: 'Mortgage',
|
||||
target: 1310.47,
|
||||
spent: 1310.47,
|
||||
},
|
||||
]
|
||||
const navItems = [
|
||||
{
|
||||
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='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>
|
||||
),
|
||||
},
|
||||
{
|
||||
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 budgets: Budget[] = [
|
||||
{
|
||||
name: 'Groceries',
|
||||
target: 1000,
|
||||
spent: 367.97,
|
||||
},
|
||||
{
|
||||
name:
|
||||
'A very long budget name that should definitely be shortened to something reasonable',
|
||||
target: 100000000,
|
||||
spent: 26893085.56,
|
||||
},
|
||||
{
|
||||
name: 'Fast Food',
|
||||
target: 300,
|
||||
spent: 420.69,
|
||||
},
|
||||
{
|
||||
name: 'Insurance',
|
||||
target: 220.44,
|
||||
spent: 0,
|
||||
},
|
||||
{
|
||||
name: 'Mortgage',
|
||||
target: 1310.47,
|
||||
spent: 1310.47,
|
||||
},
|
||||
]
|
||||
|
||||
export function BudgetsOverview() {
|
||||
let sig = useSignal(IS_BROWSER ? globalThis.location.hash : '')
|
||||
|
||||
addEventListener('popstate', (ev) => {
|
||||
console.log('popstate', ev)
|
||||
sig.value = globalThis.location.hash
|
||||
ev.preventDefault()
|
||||
return false
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
console.log('Timeout!')
|
||||
}, 2000)
|
||||
addEventListener('popstate', (ev) => {
|
||||
console.log('popstate', ev)
|
||||
sig.value = globalThis.location.hash
|
||||
})
|
||||
}, [sig])
|
||||
|
||||
const uncategorizedSpend = 851.22
|
||||
|
||||
|
@ -100,9 +61,10 @@ export default function Dashboard() {
|
|||
<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'>
|
||||
<main class='p-2 sm:pr-0 flex flex-col gap-2 max-w-full' f-client-nav>
|
||||
{/*<h1>Budgets Overview</h1>*/}
|
||||
<section class='flex flex-col gap-2 max-w-full'>
|
||||
{uncategorizedSpend <= 0 ? '' : (
|
||||
|
@ -123,21 +85,7 @@ export default function Dashboard() {
|
|||
{budgets.map((budget, _i) => <BudgetCard budget={budget} />)}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<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>
|
||||
<Nav />
|
||||
</section>
|
||||
</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 { Partial } from '$fresh/runtime.ts'
|
||||
export default function App({ Component }: PageProps) {
|
||||
return (
|
||||
<html>
|
||||
|
@ -8,8 +9,13 @@ export default function App({ Component }: PageProps) {
|
|||
<title>Bank</title>
|
||||
<link rel='stylesheet' href='/styles.css' />
|
||||
</head>
|
||||
<body class='bg-bg text-text w-screen min-w-full min-h-dvh overflow-hidden'>
|
||||
<Component />
|
||||
<body
|
||||
class='bg-bg text-text w-screen min-w-full min-h-dvh overflow-hidden'
|
||||
f-client-nav
|
||||
>
|
||||
<Partial name='body'>
|
||||
<Component />
|
||||
</Partial>
|
||||
</body>
|
||||
</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 default function Home() {
|
||||
return <Dashboard />
|
||||
export function handler(_req: Request): Response {
|
||||
return new Response('', {
|
||||
status: 307,
|
||||
headers: { Location: '/bank' },
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue