No overflow pls
This commit is contained in:
parent
cc539727ca
commit
23728881bd
9 changed files with 192 additions and 101 deletions
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>
|
||||
)
|
||||
}
|
26
deno.json
26
deno.json
|
@ -9,8 +9,17 @@
|
|||
"preview": "deno run -A main.ts",
|
||||
"update": "deno run -A -r https://fresh.deno.dev/update ."
|
||||
},
|
||||
"lint": { "rules": { "tags": ["fresh", "recommended"] } },
|
||||
"exclude": ["**/_fresh/*"],
|
||||
"lint": {
|
||||
"rules": {
|
||||
"tags": [
|
||||
"fresh",
|
||||
"recommended"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"**/_fresh/*"
|
||||
],
|
||||
"imports": {
|
||||
"$fresh/": "https://deno.land/x/fresh@1.6.8/",
|
||||
"preact": "https://esm.sh/preact@10.19.6",
|
||||
|
@ -22,7 +31,14 @@
|
|||
"tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js",
|
||||
"$std/": "https://deno.land/std@0.216.0/"
|
||||
},
|
||||
"compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" },
|
||||
"fmt": { "useTabs": true, "semiColons": false, "singleQuote": true },
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact"
|
||||
},
|
||||
"fmt": {
|
||||
"useTabs": true,
|
||||
"semiColons": false,
|
||||
"singleQuote": 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 $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 { type Manifest } from '$fresh/server.ts'
|
||||
|
@ -20,6 +21,7 @@ const manifest = {
|
|||
'./routes/index.tsx': $index,
|
||||
},
|
||||
islands: {
|
||||
'./islands/BudgetCard.tsx': $BudgetCard,
|
||||
'./islands/Counter.tsx': $Counter,
|
||||
'./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 { JSX } from 'preact'
|
||||
|
||||
import { Budget, currency, percentage } from '../models.ts'
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
import { Budget } from '../models.ts'
|
||||
import { BudgetCard } from './BudgetCard.tsx'
|
||||
import { IconButton } from '../components/IconButton.tsx'
|
||||
|
||||
export default function Dashboard() {
|
||||
const activeNavItemIndex = useSignal(0)
|
||||
|
@ -67,6 +11,12 @@ export default function Dashboard() {
|
|||
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,
|
||||
|
@ -144,25 +94,33 @@ export default function Dashboard() {
|
|||
]
|
||||
|
||||
return (
|
||||
<div class='grid grid-rows-[1fr_auto] sm:grid-cols-2 sm:grid-rows-1 sm:grid-cols-[1fr_auto] h-[100vh]'>
|
||||
<main class='p-2 sm:pr-0 flex flex-col gap-2'>
|
||||
{budgets.map((budget, _i) => <BudgetCard budget={budget} />)}
|
||||
</main>
|
||||
<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>
|
||||
</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'>
|
||||
{navItems.map(({ text, ...props }, i) => (
|
||||
<IconButton
|
||||
{...props}
|
||||
active={i == activeNavItemIndex.value}
|
||||
onClick={(ev) => {
|
||||
console.log({ ev, i })
|
||||
activeNavItemIndex.value = i
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</IconButton>
|
||||
))}
|
||||
</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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
18
models.ts
18
models.ts
|
@ -16,21 +16,3 @@ export interface Transaction {
|
|||
budget: string
|
||||
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>
|
||||
<link rel='stylesheet' href='/styles.css' />
|
||||
</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 />
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in a new issue