No overflow pls
This commit is contained in:
parent
cc539727ca
commit
23728881bd
20
components/Currency.tsx
Normal file
20
components/Currency.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { JSX } from 'preact/jsx-runtime'
|
||||||
|
|
||||||
|
export const currencyFormat = new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'USD',
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
export function Currency(
|
||||||
|
{ amount, ...props }:
|
||||||
|
& { amount: number }
|
||||||
|
& JSX.HTMLAttributes<HTMLSpanElement>,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<span class='font-mono' {...props}>
|
||||||
|
{currencyFormat.format(amount)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
24
components/IconButton.tsx
Normal file
24
components/IconButton.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { JSX } from 'preact/jsx-runtime'
|
||||||
|
|
||||||
|
interface IconButtonProps {
|
||||||
|
active?: boolean
|
||||||
|
text?: string
|
||||||
|
icon?: JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconButton(
|
||||||
|
{ icon, children, active, ...props }:
|
||||||
|
& IconButtonProps
|
||||||
|
& JSX.HTMLAttributes<HTMLButtonElement>,
|
||||||
|
) {
|
||||||
|
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>
|
||||||
|
<div class=''>{children}</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
23
components/Percentage.tsx
Normal file
23
components/Percentage.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
const percentageFormat = new Intl.NumberFormat('en-US', {
|
||||||
|
currency: 'USD',
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
})
|
||||||
|
export function percentage(ratio: number): string {
|
||||||
|
return percentageFormat.format(ratio * 100) + '%'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clamp(min: number, val: number, max: number): number {
|
||||||
|
return Math.max(min, Math.min(max, val))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Percentage(
|
||||||
|
{ ratio, ...props }:
|
||||||
|
& { ratio: number }
|
||||||
|
& JSX.HTMLAttributes<HTMLSpanElement>,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<span class='font-mono' {...props}>
|
||||||
|
{percentage(ratio)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
24
deno.json
24
deno.json
|
@ -9,8 +9,17 @@
|
||||||
"preview": "deno run -A main.ts",
|
"preview": "deno run -A main.ts",
|
||||||
"update": "deno run -A -r https://fresh.deno.dev/update ."
|
"update": "deno run -A -r https://fresh.deno.dev/update ."
|
||||||
},
|
},
|
||||||
"lint": { "rules": { "tags": ["fresh", "recommended"] } },
|
"lint": {
|
||||||
"exclude": ["**/_fresh/*"],
|
"rules": {
|
||||||
|
"tags": [
|
||||||
|
"fresh",
|
||||||
|
"recommended"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"**/_fresh/*"
|
||||||
|
],
|
||||||
"imports": {
|
"imports": {
|
||||||
"$fresh/": "https://deno.land/x/fresh@1.6.8/",
|
"$fresh/": "https://deno.land/x/fresh@1.6.8/",
|
||||||
"preact": "https://esm.sh/preact@10.19.6",
|
"preact": "https://esm.sh/preact@10.19.6",
|
||||||
|
@ -22,7 +31,14 @@
|
||||||
"tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js",
|
"tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js",
|
||||||
"$std/": "https://deno.land/std@0.216.0/"
|
"$std/": "https://deno.land/std@0.216.0/"
|
||||||
},
|
},
|
||||||
"compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" },
|
"compilerOptions": {
|
||||||
"fmt": { "useTabs": true, "semiColons": false, "singleQuote": true },
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "preact"
|
||||||
|
},
|
||||||
|
"fmt": {
|
||||||
|
"useTabs": true,
|
||||||
|
"semiColons": false,
|
||||||
|
"singleQuote": true
|
||||||
|
},
|
||||||
"nodeModulesDir": true
|
"nodeModulesDir": true
|
||||||
}
|
}
|
|
@ -7,6 +7,7 @@ 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 $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 $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 { type Manifest } from '$fresh/server.ts'
|
import { type Manifest } from '$fresh/server.ts'
|
||||||
|
@ -20,6 +21,7 @@ const manifest = {
|
||||||
'./routes/index.tsx': $index,
|
'./routes/index.tsx': $index,
|
||||||
},
|
},
|
||||||
islands: {
|
islands: {
|
||||||
|
'./islands/BudgetCard.tsx': $BudgetCard,
|
||||||
'./islands/Counter.tsx': $Counter,
|
'./islands/Counter.tsx': $Counter,
|
||||||
'./islands/Dashboard.tsx': $Dashboard,
|
'./islands/Dashboard.tsx': $Dashboard,
|
||||||
},
|
},
|
||||||
|
|
66
islands/BudgetCard.tsx
Normal file
66
islands/BudgetCard.tsx
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import { JSX } from 'preact/jsx-runtime'
|
||||||
|
import { Budget } from '../models.ts'
|
||||||
|
import { Currency } from '../components/Currency.tsx'
|
||||||
|
import { Percentage } from '../components/Percentage.tsx'
|
||||||
|
|
||||||
|
export function BudgetCard(
|
||||||
|
{ budget, ...props }:
|
||||||
|
& { budget: Budget }
|
||||||
|
& JSX.HTMLAttributes<HTMLDivElement>,
|
||||||
|
) {
|
||||||
|
const remainingRatio = 1.0 - (budget.spent / budget.target)
|
||||||
|
const bgColor = remainingRatio < 0 ? 'bg-red' : 'bg-green'
|
||||||
|
const textColor = remainingRatio < 0 ? 'text-red' : 'text-green'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class='bg-surface0 rounded shadow hover:bg-surface1 transition-colors flex justify-between relative z-0 cursor-pointer'
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class={`rounded absolute top-0 bottom-0 left-0 ${bgColor} z-[-1] opacity-10`}
|
||||||
|
style={`right: ${
|
||||||
|
(remainingRatio < 0 ? 0 : (Math.abs(1.0 - remainingRatio))) * 100
|
||||||
|
}%;`}
|
||||||
|
/>
|
||||||
|
<h2
|
||||||
|
class={`py-2 px-3 text-2xl flex flex-none max-w-xs items-center ${textColor} truncate`}
|
||||||
|
>
|
||||||
|
{budget.name}
|
||||||
|
</h2>
|
||||||
|
<div class={`p-2 pl-0 flex-1 leading-none`}>
|
||||||
|
<span class={textColor}>
|
||||||
|
<Currency amount={Math.abs(budget.target - budget.spent)} />
|
||||||
|
{remainingRatio >= 0 ? ` left ` : ` over `}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<small>
|
||||||
|
<Currency amount={budget.spent} /> of{' '}
|
||||||
|
<Currency amount={budget.target} />
|
||||||
|
{' spent '}
|
||||||
|
(<Percentage ratio={remainingRatio} /> remaining)
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
<button class='text-surface1 cursor-grab p-2'>
|
||||||
|
<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='M3.75 9h16.5m-16.5 6.75h16.5'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,63 +1,7 @@
|
||||||
import { useSignal } from 'https://esm.sh/v135/@preact/signals@1.2.2/X-ZS8q/dist/signals.js'
|
import { useSignal } from 'https://esm.sh/v135/@preact/signals@1.2.2/X-ZS8q/dist/signals.js'
|
||||||
import { JSX } from 'preact'
|
import { Budget } from '../models.ts'
|
||||||
|
import { BudgetCard } from './BudgetCard.tsx'
|
||||||
import { Budget, currency, percentage } from '../models.ts'
|
import { IconButton } from '../components/IconButton.tsx'
|
||||||
|
|
||||||
interface IconButtonProps {
|
|
||||||
active?: boolean
|
|
||||||
text?: string
|
|
||||||
icon?: JSX.Element
|
|
||||||
}
|
|
||||||
function IconButton(
|
|
||||||
{ icon, children, active, ...props }:
|
|
||||||
& IconButtonProps
|
|
||||||
& JSX.HTMLAttributes<HTMLButtonElement>,
|
|
||||||
) {
|
|
||||||
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>
|
|
||||||
<div class=''>{children}</div>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BudgetCard(
|
|
||||||
{ budget, ...props }:
|
|
||||||
& { budget: Budget }
|
|
||||||
& JSX.HTMLAttributes<HTMLDivElement>,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class='bg-surface0 rounded shadow hover:bg-surface1 transition-colors flex justify-between'
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div class='p-2'>
|
|
||||||
{budget.name}: {currency(budget.spent)} / {currency(budget.target)}{' '}
|
|
||||||
({percentage(budget.spent / budget.target)})
|
|
||||||
</div>
|
|
||||||
<button class='text-surface1 cursor-grab p-2'>
|
|
||||||
<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='M3.75 9h16.5m-16.5 6.75h16.5'
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const activeNavItemIndex = useSignal(0)
|
const activeNavItemIndex = useSignal(0)
|
||||||
|
@ -67,6 +11,12 @@ export default function Dashboard() {
|
||||||
target: 1000,
|
target: 1000,
|
||||||
spent: 367.97,
|
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',
|
name: 'Fast Food',
|
||||||
target: 300,
|
target: 300,
|
||||||
|
@ -144,25 +94,33 @@ export default function Dashboard() {
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class='grid grid-rows-[1fr_auto] sm:grid-cols-2 sm:grid-rows-1 sm:grid-cols-[1fr_auto] h-[100vh]'>
|
<div class='h-[100vh] flex flex-col max-w-full'>
|
||||||
<main class='p-2 sm:pr-0 flex flex-col gap-2'>
|
<header class='bg-mantle'>
|
||||||
{budgets.map((budget, _i) => <BudgetCard budget={budget} />)}
|
<h1 class='text-2xl font-mono p-2 px-2'>FlanBank</h1>
|
||||||
</main>
|
</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'>
|
||||||
|
<main class='p-2 sm:pr-0 flex flex-col gap-2 max-w-full'>
|
||||||
|
{/*<h1>Budgets Overview</h1>*/}
|
||||||
|
<section class='flex flex-col gap-2 max-w-full'>
|
||||||
|
{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'>
|
<nav class='w-full flex flex-1 sm:flex-col sm:bg-bg gap-2 p-2'>
|
||||||
{navItems.map(({ text, ...props }, i) => (
|
{navItems.map(({ text, ...props }, i) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
{...props}
|
{...props}
|
||||||
active={i == activeNavItemIndex.value}
|
active={i == activeNavItemIndex.value}
|
||||||
onClick={(ev) => {
|
onClick={(ev) => {
|
||||||
console.log({ ev, i })
|
console.log({ ev, i })
|
||||||
activeNavItemIndex.value = i
|
activeNavItemIndex.value = i
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
18
models.ts
18
models.ts
|
@ -16,21 +16,3 @@ export interface Transaction {
|
||||||
budget: string
|
budget: string
|
||||||
amount: number
|
amount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const currencyFormat = new Intl.NumberFormat('en-US', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'USD',
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
})
|
|
||||||
export function currency(amount: number): string {
|
|
||||||
return currencyFormat.format(amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
const percentageFormat = new Intl.NumberFormat('en-US', {
|
|
||||||
currency: 'USD',
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
})
|
|
||||||
export function percentage(ratio: number): string {
|
|
||||||
return percentageFormat.format(ratio * 100) + '%'
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ 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 min-w-full min-h-dvh'>
|
<body class='bg-bg text-text w-screen min-w-full min-h-dvh overflow-hidden'>
|
||||||
<Component />
|
<Component />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in a new issue