mirror of
https://github.com/spliit-app/spliit.git
synced 2026-02-17 21:16:14 +01:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cd2b273f9 | ||
|
|
1ad470309b | ||
|
|
2fd38aadd9 | ||
|
|
b61d1836ea | ||
|
|
c3903849ec |
42
.devcontainer/devcontainer.json
Normal file
42
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,42 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node-postgres
|
||||
{
|
||||
"name": "spliit",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {
|
||||
// "ghcr.io/frntn/devcontainers-features/prism:1": {}
|
||||
// },
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "cp container.env.example .env && npm install",
|
||||
"postAttachCommand": {
|
||||
"npm": "npm run dev"
|
||||
},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// This can be used to network with other containers or with the host.
|
||||
"forwardPorts": [3000, 5432],
|
||||
"portsAttributes": {
|
||||
"3000": {
|
||||
"label": "App"
|
||||
},
|
||||
"5432": {
|
||||
"label": "PostgreSQL"
|
||||
}
|
||||
},
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
"codespaces": {
|
||||
"openFiles": [
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
33
.devcontainer/docker-compose.yml
Normal file
33
.devcontainer/docker-compose.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
image: mcr.microsoft.com/devcontainers/typescript-node:latest
|
||||
|
||||
volumes:
|
||||
- ../..:/workspaces:cached
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: sleep infinity
|
||||
|
||||
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
||||
network_mode: service:db
|
||||
|
||||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: 1234
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: postgres
|
||||
|
||||
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
@@ -23,6 +23,12 @@ const nextConfig = {
|
||||
images: {
|
||||
remotePatterns
|
||||
},
|
||||
// Required to run in a codespace (see https://github.com/vercel/next.js/issues/58019)
|
||||
experimental: {
|
||||
serverActions: {
|
||||
allowedOrigins: ['localhost:3000'],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Expense" ADD COLUMN "notes" TEXT;
|
||||
@@ -52,6 +52,7 @@ model Expense {
|
||||
splitMode SplitMode @default(EVENLY)
|
||||
createdAt DateTime @default(now())
|
||||
documents ExpenseDocument[]
|
||||
notes String?
|
||||
}
|
||||
|
||||
model ExpenseDocument {
|
||||
|
||||
43
src/app/groups/[groupId]/expenses/active-user-balance.tsx
Normal file
43
src/app/groups/[groupId]/expenses/active-user-balance.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client'
|
||||
import { Money } from '@/components/money'
|
||||
import { getBalances } from '@/lib/balances'
|
||||
import { useActiveUser } from '@/lib/hooks'
|
||||
|
||||
type Props = {
|
||||
groupId: string
|
||||
currency: string
|
||||
expense: Parameters<typeof getBalances>[0][number]
|
||||
}
|
||||
|
||||
export function ActiveUserBalance({ groupId, currency, expense }: Props) {
|
||||
const activeUserId = useActiveUser(groupId)
|
||||
if (activeUserId === null || activeUserId === '' || activeUserId === 'None') {
|
||||
return null
|
||||
}
|
||||
|
||||
const balances = getBalances([expense])
|
||||
let fmtBalance = <>You are not involved</>
|
||||
if (Object.hasOwn(balances, activeUserId)) {
|
||||
const balance = balances[activeUserId]
|
||||
let balanceDetail = <></>
|
||||
if (balance.paid > 0 && balance.paidFor > 0) {
|
||||
balanceDetail = (
|
||||
<>
|
||||
{' ('}
|
||||
<Money {...{ currency, amount: balance.paid }} />
|
||||
{' - '}
|
||||
<Money {...{ currency, amount: balance.paidFor }} />
|
||||
{')'}
|
||||
</>
|
||||
)
|
||||
}
|
||||
fmtBalance = (
|
||||
<>
|
||||
Your balance:{' '}
|
||||
<Money {...{ currency, amount: balance.total }} bold colored />
|
||||
{balanceDetail}
|
||||
</>
|
||||
)
|
||||
}
|
||||
return <div className="text-xs text-muted-foreground">{fmtBalance}</div>
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client'
|
||||
import { ActiveUserBalance } from '@/app/groups/[groupId]/expenses/active-user-balance'
|
||||
import { CategoryIcon } from '@/app/groups/[groupId]/expenses/category-icon'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { SearchBar } from '@/components/ui/search-bar'
|
||||
@@ -151,6 +152,9 @@ export function ExpenseList({
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<ActiveUserBalance {...{ groupId, currency, expense }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-between items-end">
|
||||
<div
|
||||
|
||||
@@ -51,6 +51,7 @@ export default async function GroupExpensesPage({
|
||||
prefetch={false}
|
||||
href={`/groups/${groupId}/expenses/export/json`}
|
||||
target="_blank"
|
||||
title="Export to JSON"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</Link>
|
||||
@@ -63,7 +64,10 @@ export default async function GroupExpensesPage({
|
||||
/>
|
||||
)}
|
||||
<Button asChild size="icon">
|
||||
<Link href={`/groups/${groupId}/expenses/create`}>
|
||||
<Link
|
||||
href={`/groups/${groupId}/expenses/create`}
|
||||
title="Create expense"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -23,7 +23,7 @@ export function ShareButton({ group }: Props) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button size="icon">
|
||||
<Button title="Share" size="icon">
|
||||
<Share className="w-4 h-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -50,6 +50,7 @@ import { useForm } from 'react-hook-form'
|
||||
import { match } from 'ts-pattern'
|
||||
import { DeletePopup } from './delete-popup'
|
||||
import { extractCategoryFromTitle } from './expense-form-actions'
|
||||
import { Textarea } from './ui/textarea'
|
||||
|
||||
export type Props = {
|
||||
group: NonNullable<Awaited<ReturnType<typeof getGroup>>>
|
||||
@@ -156,7 +157,7 @@ export function ExpenseForm({
|
||||
const getSelectedPayer = (field?: { value: string }) => {
|
||||
if (isCreate && typeof window !== 'undefined') {
|
||||
const activeUser = localStorage.getItem(`${group.id}-activeUser`)
|
||||
if (activeUser && activeUser !== 'None') {
|
||||
if (activeUser && activeUser !== 'None' && field?.value === undefined) {
|
||||
return activeUser
|
||||
}
|
||||
}
|
||||
@@ -180,6 +181,7 @@ export function ExpenseForm({
|
||||
saveDefaultSplittingOptions: false,
|
||||
isReimbursement: expense.isReimbursement,
|
||||
documents: expense.documents,
|
||||
notes: expense.notes ?? '',
|
||||
}
|
||||
: searchParams.get('reimbursement')
|
||||
? {
|
||||
@@ -202,6 +204,7 @@ export function ExpenseForm({
|
||||
splitMode: defaultSplittingOptions.splitMode,
|
||||
saveDefaultSplittingOptions: false,
|
||||
documents: [],
|
||||
notes: '',
|
||||
}
|
||||
: {
|
||||
title: searchParams.get('title') ?? '',
|
||||
@@ -228,6 +231,7 @@ export function ExpenseForm({
|
||||
},
|
||||
]
|
||||
: [],
|
||||
notes: '',
|
||||
},
|
||||
})
|
||||
const [isCategoryLoading, setCategoryLoading] = useState(false)
|
||||
@@ -400,6 +404,18 @@ export function ExpenseForm({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notes"
|
||||
render={({ field }) => (
|
||||
<FormItem className="sm:order-6">
|
||||
<FormLabel>Notes</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea className="text-base" {...field} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
31
src/components/money.tsx
Normal file
31
src/components/money.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
import { cn, formatCurrency } from '@/lib/utils'
|
||||
|
||||
type Props = {
|
||||
currency: string
|
||||
amount: number
|
||||
bold?: boolean
|
||||
colored?: boolean
|
||||
}
|
||||
|
||||
export function Money({
|
||||
currency,
|
||||
amount,
|
||||
bold = false,
|
||||
colored = false,
|
||||
}: Props) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
colored && amount <= 1
|
||||
? 'text-red-600'
|
||||
: colored && amount >= 1
|
||||
? 'text-green-600'
|
||||
: '',
|
||||
bold && 'font-bold',
|
||||
)}
|
||||
>
|
||||
{formatCurrency(currency, amount)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -72,6 +72,7 @@ export async function createExpense(
|
||||
})),
|
||||
},
|
||||
},
|
||||
notes: expenseFormValues.notes,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -185,6 +186,7 @@ export async function updateExpense(
|
||||
id: doc.id,
|
||||
})),
|
||||
},
|
||||
notes: expenseFormValues.notes,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ export const expenseFormSchema = z
|
||||
}),
|
||||
)
|
||||
.default([]),
|
||||
notes: z.string().optional(),
|
||||
})
|
||||
.superRefine((expense, ctx) => {
|
||||
let sum = 0
|
||||
|
||||
Reference in New Issue
Block a user