Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
routeTree.gen.ts
**/routeTree.gen.ts
**/*.hbs
**/.gitignore
.prettierignore
11 changes: 10 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,14 @@
"tailwindCSS.classAttributes": ["class", "className", "ngClass"],
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
],
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
},
"files.watcherExclude": {
"**/routeTree.gen.ts": true
},
"search.exclude": {
"**/routeTree.gen.ts": true
}
}
1 change: 1 addition & 0 deletions catalog/budget/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/routeTree.gen.ts
32 changes: 32 additions & 0 deletions catalog/budget/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@catalog/budget",
"version": "0.0.0",
"private": true,
"main": "./dist/index.js",
"scripts": {
"build": "vite build",
"dev": "vite build --watch"
},
"devDependencies": {
"@types/node": "^20.10.6",
"@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18",
"react": "^18.2.0",
"typescript": "^5.3.3"
},
"dependencies": {
"@coat-rack/icons": "workspace:*",
"@coat-rack/sdk": "workspace:*",
"@coat-rack/tailwind-config": "workspace:*",
"@coat-rack/ui": "workspace:*",
"@rollup/plugin-replace": "^5.0.5",
"@tanstack/react-router": "^1.121.27",
"@tanstack/router-plugin": "^1.121.27",
"@vitejs/plugin-react": "^4.2.1",
"rollup-plugin-external-globals": "^0.9.2",
"swr": "^2.3.4",
"tailwindcss": "^3.4.1",
"vite": "^5.0.8",
"vite-plugin-css-injected-by-js": "^3.5.0"
}
}
6 changes: 6 additions & 0 deletions catalog/budget/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
6 changes: 6 additions & 0 deletions catalog/budget/public/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "budget",
"id": "budget",
"version": "0.0.0",
"timestamp": 1741941240758
}
45 changes: 45 additions & 0 deletions catalog/budget/src/components/Account.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Table, TableCell, TableRow } from "@coat-rack/ui/components/table"
import { useCurrencyFormatter } from "../format"
import { AccountInstance, TransactionItem } from "../models"

export interface AccountProps {
account: AccountInstance
}

export function Account({ account }: AccountProps) {
const formatter = useCurrencyFormatter()
const dates = Array.from(
account.transactions.reduce((dates, curr) => {
let transactions = dates.get(curr.date)
if (!transactions) {
transactions = []
dates.set(curr.date, transactions)
}
transactions.push(curr)
return dates
}, new Map<Date, TransactionItem[]>()),
)
return (
<Table>
{dates.map(([date, transactions]) => (
<>
<TableRow>
<TableCell>{date.toDateString()}</TableCell>
<TableCell>{/* empty space for amount */}</TableCell>
</TableRow>
{transactions.map((tran) => (
<TableRow>
<TableCell>
<div>
<div>{tran.payee}</div>
<div>{tran.description}</div>
</div>
</TableCell>
<TableCell>{formatter.format(tran.amount)}</TableCell>
</TableRow>
))}
</>
))}
</Table>
)
}
32 changes: 32 additions & 0 deletions catalog/budget/src/components/Budget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
Table,
TableBody,
TableHead,
TableHeader,
TableRow,
} from "@coat-rack/ui/components/table"
import { BudgetInstance } from "../models"
import { CategoryGroup } from "./CategoryGroup"

export interface BudgetProps {
data: BudgetInstance
}
export function Budget({ data }: BudgetProps) {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead>Category</TableHead>
<TableHead>Assigned</TableHead>
<TableHead>Spent</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.categoryGroups.map((g) => (
<CategoryGroup key={g.name} categoryGroup={g} />
))}
</TableBody>
</Table>
)
}
28 changes: 28 additions & 0 deletions catalog/budget/src/components/Category.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Progress } from "@coat-rack/ui/components/progress"
import { TableCell, TableRow } from "@coat-rack/ui/components/table"
import { useCurrencyFormatter } from "../format"
import { CategoryItem } from "../models"

export interface CategoryViewProps {
category: CategoryItem
}

export function Category({ category }: CategoryViewProps) {
const formatter = useCurrencyFormatter()
return (
<TableRow key={category.name}>
<TableCell>{/* Empty space for chevron */}</TableCell>
<TableCell>
<div>{category.name}</div>
<div>
<Progress
value={(category.spentAmount / category.assignedAmount) * 100}
size={"slim"}
></Progress>
</div>
</TableCell>
<TableCell>{formatter.format(category.assignedAmount)}</TableCell>
<TableCell>{formatter.format(category.spentAmount)}</TableCell>
</TableRow>
)
}
52 changes: 52 additions & 0 deletions catalog/budget/src/components/CategoryGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ChevronDown, ChevronUp } from "@coat-rack/icons/regular"
import { Button } from "@coat-rack/ui/components/button"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@coat-rack/ui/components/collapsible"
import { TableCell, TableRow } from "@coat-rack/ui/components/table"
import { useState } from "react"
import { useCurrencyFormatter } from "../format"
import { CategoryGroupItem } from "../models"
import { Category } from "./Category"

export interface CategoryGroupViewProps {
categoryGroup: CategoryGroupItem
}

export function CategoryGroup({ categoryGroup }: CategoryGroupViewProps) {
const totalAssigned = categoryGroup.categories.reduce(
(sum, category) => (sum += category.assignedAmount),
0,
)
const totalSpent = categoryGroup.categories.reduce(
(sum, category) => (sum += category.spentAmount),
0,
)

const [open, setOpen] = useState(true)

const amountFormatter = useCurrencyFormatter()
return (
<Collapsible open={open} onOpenChange={setOpen} asChild>
<>
<TableRow>
<TableCell>
<CollapsibleTrigger>
<Button asChild>{open ? <ChevronDown /> : <ChevronUp />}</Button>
</CollapsibleTrigger>
</TableCell>
<TableCell>{categoryGroup.name}</TableCell>
<TableCell>{amountFormatter.format(totalAssigned)}</TableCell>
<TableCell>{amountFormatter.format(totalSpent)}</TableCell>
</TableRow>
{open &&
categoryGroup.categories.map((category) => (
<Category key={category.name} category={category} />
))}
<CollapsibleContent></CollapsibleContent>
</>
</Collapsible>
)
}
31 changes: 31 additions & 0 deletions catalog/budget/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createContext, useContext } from "react"

export type LocaleContext = Intl.LocalesArgument

const LocaleContext = createContext<LocaleContext>(
undefined as unknown as LocaleContext,
)

export const LocaleProvider = LocaleContext.Provider

export const useLocale = () => {
const context = useContext(LocaleContext)
if (!context) {
throw new Error("Locale not set")
}
return context
}

export type CurrencyContext = string

const CurrencyContext = createContext<CurrencyContext>(
undefined as unknown as CurrencyContext,
)
export const useCurrency = () => {
const context = useContext(CurrencyContext)
if (!context) {
throw new Error("Currency not set")
}
return context
}
export const CurrencyProvider = CurrencyContext.Provider
7 changes: 7 additions & 0 deletions catalog/budget/src/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useCurrency, useLocale } from "./context"

export function useCurrencyFormatter() {
const locale = useLocale()
const currency = useCurrency()
return Intl.NumberFormat(locale, { style: "currency", currency })
}
28 changes: 28 additions & 0 deletions catalog/budget/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { App, ProvideAppContext } from "@coat-rack/sdk"

import { createRouter, RouterProvider } from "@tanstack/react-router"
import { DbTypes } from "./models"
import { routeTree } from "./routeTree.gen"
import "./styles.css"

const router = createRouter({ routeTree })
declare module "@tanstack/react-router" {
interface Register {
router: typeof router
}
}

export const BudgetApp: App<DbTypes> = {
/**
* The Entrypoint for the app
*/
Entry: ({ context }) => {
return (
<ProvideAppContext {...context}>
<RouterProvider router={router} context={context}></RouterProvider>
</ProvideAppContext>
)
},
}

export default BudgetApp
71 changes: 71 additions & 0 deletions catalog/budget/src/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { DbRecord } from "@coat-rack/sdk"

export enum CategoryKind {
ReadyToAssign = 1,
Normal = 2,
}
export interface Category extends Model<"category"> {
name: string
group: DbRef<CategoryGroup>
target?: { amount: number; date?: Date }
kind: CategoryKind
}

export interface CategoryGroup extends Model<"categorygroup"> {
name: string
}

export interface Budget extends Model<"budget"> {
locale: string
currency: string
name: string
}

export interface BudgetCycle extends Model<"budgetcycle"> {
startDate: Date
endDate: Date
assignments: Record<DbRef<Category>, { assignedAmount: number }>
budget: DbRef<Budget>
}

export interface Account extends Model<"account"> {
name: string
}

export interface Transaction extends Model<"transaction"> {
amount: number
payee: string
description: string
date: Date
account: DbRef<Account>
category: DbRef<Category>
}

export interface Remember {
lastOpenedBudget?: DbRef<Budget>
}

const Brand = Symbol()
type Brand = typeof Brand
type DbRef<T extends Model<string>> = {
[Brand]: T["type"]
} & string

export function getDbRef<T extends Model<string>>(
model: DbRecord<T>,
): DbRef<T> {
return model.id as DbRef<T>
}

type Model<T extends string> = {
type: T
}

export type DbTypes =
| Category
| CategoryGroup
| Budget
| BudgetCycle
| Account
| Transaction
| Remember
14 changes: 14 additions & 0 deletions catalog/budget/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { AppContext } from "@coat-rack/sdk"
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"

function BudgetAppRoot() {
return (
<>
<Outlet />
</>
)
}

export const Route = createRootRouteWithContext<AppContext>()({
component: BudgetAppRoot,
})
9 changes: 9 additions & 0 deletions catalog/budget/src/routes/account/$accountId.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router"

export const Route = createFileRoute("/account/$accountId")({
component: RouteComponent,
})

function RouteComponent() {
return <div>Hello "/account/$accountId"!</div>
}
Loading