No overflow pls

This commit is contained in:
Daniel Flanagan 2024-08-24 11:31:45 -05:00
parent cc539727ca
commit 23728881bd
9 changed files with 192 additions and 101 deletions

20
components/Currency.tsx Normal file
View 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
View 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
View 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>
)
}

View file

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

View file

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

View file

@ -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,9 +94,16 @@ 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'>
<h1 class='text-2xl font-mono p-2 px-2'>FlanBank</h1>
</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} />)} {budgets.map((budget, _i) => <BudgetCard budget={budget} />)}
</section>
</main> </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'>
@ -163,6 +120,7 @@ export default function Dashboard() {
</IconButton> </IconButton>
))} ))}
</nav> </nav>
</section>
</div> </div>
) )
} }

View file

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

View file

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