production test

This commit is contained in:
mr-shortman 2025-05-03 15:06:07 +02:00
parent 76f1685c71
commit a945005e76
18 changed files with 123 additions and 188 deletions

15
Dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM node:18-alpine
RUN npm install -g pnpm
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
CMD ["pnpm", "start"]

View File

@ -1,9 +0,0 @@
import type { CapacitorConfig } from "@capacitor/cli";
const config: CapacitorConfig = {
appId: "bettersplit.shrt.solutions",
appName: "bettersplit",
webDir: "out",
};
export default config;

39
docker-compose.yml Normal file
View File

@ -0,0 +1,39 @@
services:
nextapp:
build: .
container_name: bettersplit
restart: always
ports:
- "3000:3000"
expose:
- "3000"
labels:
- "traefik.enable=true"
- "traefik.http.routers.bettersplit.rule=Host(`bettersplit.shortman.me`)"
- "traefik.http.routers.bettersplit.entrypoints=websecure"
- "traefik.http.routers.bettersplit.tls.certresolver=myresolver"
networks:
- bettersplit_network
- webproxy
db:
image: postgres:latest
container_name: bettersplit-db
restart: always
shm_size: 128mb
env_file:
- .env.production
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- bettersplit_network
volumes:
pgdata:
driver: local
networks:
bettersplit_network:
driver: bridge
webproxy:
external: true

BIN
public/icon-192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
public/icon-512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,12 @@
type AppConfig = {
name: string;
shortName: string;
navigator: Array<NavLink>;
};
export const appConfig: AppConfig = {
name: "Bettesplit",
shortName: "BS",
navigator: [
{
name: "Home",

View File

@ -12,6 +12,7 @@ import React from "react";
export default async function Page() {
const splits = await api.expense.getAll();
const sessionUser = await currentUser();
return (
<>
<Header text="Expenses">
@ -20,26 +21,6 @@ export default async function Page() {
</Button>
</Header>
<Section>
{/* STATS */}
{/* <div className=" rounded-md flex items-center h-20 bg-card/25">
<div className="p-4 space-y-1 text-center w-full">
<h4 className="text-muted-foreground text-sm">Expenses</h4>
<span className="text-xl font-bold">{splits.length}</span>
</div>
<Separator orientation="vertical" />
<div className="p-4 space-y-1 text-center w-full">
<h4 className="text-muted-foreground text-sm">Expenses</h4>
<span className="text-xl font-bold">{splits.length}</span>
</div>
<Separator orientation="vertical" />
<div className="p-4 space-y-1 text-center w-full">
<h4 className="text-muted-foreground text-sm ">Balance</h4>
<span className="text-xl font-bold text-success">
{getAmount(812.47)}
</span>
</div>
</div> */}
{/* FILTER BAR */}
<div className="py-4 flex items-center gap-2">
<Button

View File

@ -1,6 +1,7 @@
import React from "react";
import GroupPage from "@/app/_components/group/group-page";
import { api, HydrateClient } from "@/trpc/server";
import { currentUser } from "@clerk/nextjs/server";
async function Page({
params,
@ -11,10 +12,11 @@ async function Page({
}) {
const { id } = await params;
void api.group.get.prefetch({ id });
const sessionUser = await currentUser();
return (
<HydrateClient>
<GroupPage groupId={id} />
<GroupPage sessionUserId={sessionUser?.id!} groupId={id} />
</HydrateClient>
);
}

View File

@ -17,11 +17,11 @@ export default async function Home() {
<Header
text={
<>
<Icons.logo
{/* <Icons.logo
className="size-32 scale-125 mr-2 absolute -left-4 -top-1/2 text-muted-foreground/5 -z-10
mask-r-from-30%
"
/>
/> */}
<span>{appConfig.name}</span>
<ModeToggle className="ml-4" />
</>

View File

@ -21,23 +21,8 @@ function ExpensePage({ sessionUser }: { sessionUser: User }) {
const groupBadges = useExpenseStore((state) => state.groupBadges);
const addGroupBadges = useExpenseStore((state) => state.addGroupBadges);
const searchParams = useSearchParams();
const paramGroup = searchParams.get("groupId");
const paramUser = searchParams.get("userId");
// React.useEffect(() => {
// console.log("Params: ", paramGroup, paramUser);
// if (paramGroup?.length) {
// if (!groupBadges.find((group) => group.id === paramGroup)) {
// const { data: group } = api.group.get.useQuery({
// id: paramGroup,
// });
// if (group) addGroupBadges([group]);
// if (group?.members?.length)
// addParticipants(group.members.map(({ user }) => user));
// }
// }
// }, [paramGroup, paramUser]);
return (
<>
<Header text="Add Expense">

View File

@ -1,22 +1,10 @@
"use client";
import React from "react";
import FriendSelect from "../friend/friend-select";
import { Button } from "@/components/ui/button";
import { Icons } from "@/components/icons";
import { useExpenseStore } from "@/lib/store/expense-store";
import PaidByInput from "./paid-by";
import { api } from "@/trpc/react";
import SplitTo from "./split-to";
import SplitBetweenInput from "./split-between-input";
import type { User } from "@/server/db/schema";
export default function ExpenseSplit({ sessionUser }: { sessionUser: User }) {
const [friends] = api.friend.getAll.useSuspenseQuery();
const friendTarget = useExpenseStore((state) => state.friendTarget);
const setFriendTarget = useExpenseStore((state) => state.setFriendTarget);
const addParticipant = useExpenseStore((state) => state.addParticipant);
return (
<div className="space-y-4 ">
<div className="flex items-center justify-center gap-2">
@ -25,81 +13,8 @@ export default function ExpenseSplit({ sessionUser }: { sessionUser: User }) {
</div>
<div className="flex items-center justify-center gap-2">
<span>and spliited</span>
<SplitTo />
<SplitBetweenInput />
</div>
{/* <Separator className=" bg-primary w-full h-px my-12" />
<Select
value={splitType}
onValueChange={(currentValue) =>
setSplitType(currentValue as SplitType)
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select Split" />
</SelectTrigger>
<SelectContent>
{splitTypeKeys.map((splitType) => (
<SelectItem key={splitType} value={splitType}>
Split {splitType}
</SelectItem>
))}
</SelectContent>
</Select>
<h4 className="text-lg">Owed to</h4>
<ul className="space-y-2">
{participants.map((user) => (
<li key={user.id}>
<FriendCard
user={user}
className="border bg-background relative pr-6"
>
{session.user.id !== user.id ? (
<Button
variant={"destructive"}
className="p-0 w-6 h-6 rounded-full absolute top-1/2 -translate-y-1/2 transform -right-2"
onClick={(e) => {
e.preventDefault();
removeParticipant(user.id!);
}}
>
<Icons.trash className="size-4" />
</Button>
) : null}
<div className="flex ml-auto">
<p className="text-muted-foreground">{splits[user.id!]}</p>
</div>
</FriendCard>
</li>
))}
</ul>
<h4 className="text-lg">Paid from</h4>
<ul className="space-y-2">
{participants.map((user) => (
<li key={user.id}>
<FriendCard
user={user}
className="border bg-background relative pr-6"
>
{session.user.id !== user.id ? (
<Button
variant={"destructive"}
className="p-0 w-6 h-6 rounded-full absolute top-1/2 -translate-y-1/2 transform -right-2"
onClick={(e) => {
e.preventDefault();
removeParticipant(user.id!);
}}
>
<Icons.trash className="size-4" />
</Button>
) : null}
<div className="flex ml-auto">
<p className="text-muted-foreground">{splits[user.id!]}</p>
</div>
</FriendCard>
</li>
))}
</ul> */}
</div>
);
}

View File

@ -10,10 +10,8 @@ import {
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { DialogClose } from "@radix-ui/react-dialog";
import type { PublicUser } from "next-auth";
import Avatar from "@/components/avatar";
import React from "react";
import { Input } from "@/components/ui/input";
import { NumberInput } from "@/components/number-input";
import { EuroIcon } from "lucide-react";
import { cn } from "@/lib/utils";

View File

@ -10,8 +10,11 @@ import {
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { useExpenseStore } from "@/lib/store/expense-store";
function SplitTo() {
function SplitBetweenInput() {
const participants = useExpenseStore((state) => state.participants);
const splitType = useExpenseStore((state) => state.splitType);
return (
<Dialog>
<DialogTrigger className="flex items-center flex-wrap gap-2" asChild>
@ -25,7 +28,7 @@ function SplitTo() {
>
<DialogHeader>
<div className="flex items-center mb-8">
<DialogTitle className="text-2xl">Who paid?</DialogTitle>
<DialogTitle className="text-2xl">Split between</DialogTitle>
<DialogClose asChild>
<Button className="ml-auto">Go Back</Button>
</DialogClose>
@ -36,4 +39,4 @@ function SplitTo() {
);
}
export default SplitTo;
export default SplitBetweenInput;

View File

@ -23,33 +23,29 @@ function GroupCard({ group }: { group: Group }) {
<Icons.group className="size-4" />
<span>{group.name}</span>
</CardTitle>
<ul className="flex ">
{group?.members?.slice(0, 4)?.map(({ user }, idx) => (
<li
key={idx}
style={{
transform: `translateX(-${idx * 4}px)`,
}}
>
<Avatar
src={user?.image}
fb={user?.name}
className="size-4"
/>
</li>
))}
{Number(group?.members?.length) > 4 && (
<li
style={{
transform: `translateX(-20px)`,
}}
className="size-4 bg-foreground text-background text-xs items-center justify-center rounded-full "
>
<span>+{Number(group?.members?.length) - 4}</span>
</li>
)}
</ul>
</div>
<ul className="flex ">
{group?.members?.slice(0, 4)?.map(({ user }, idx) => (
<li
key={idx}
style={{
transform: `translateX(-${idx * 4}px)`,
}}
>
<Avatar src={user?.image} fb={user?.name} className="size-4" />
</li>
))}
{Number(group?.members?.length) > 4 && (
<li
style={{
transform: `translateX(-20px)`,
}}
className="size-4 bg-foreground text-background text-xs items-center justify-center rounded-full "
>
<span>+{Number(group?.members?.length) - 4}</span>
</li>
)}
</ul>
<CardDescription>
<p>{group.description}</p>
</CardDescription>

View File

@ -13,10 +13,15 @@ import { Button } from "@/components/ui/button";
import { Icons } from "@/components/icons";
import ExpenseCard from "../expense/expense-card";
function GroupPage({ groupId }: { groupId: string }) {
function GroupPage({
groupId,
sessionUserId,
}: {
groupId: string;
sessionUserId: string;
}) {
const [group] = api.group.get.useSuspenseQuery({ id: groupId });
if (!group) return <GroupFormDrawer />;
const groupExpenses = group?.groupBadges?.map(({ expense }) => expense);
return (
<>
@ -63,21 +68,12 @@ function GroupPage({ groupId }: { groupId: string }) {
<ul className="space-y-2">
{groupExpenses.map((expense) => (
<ExpenseCard key={expense.id} expense={expense} />
<ExpenseCard
key={expense.id}
sessionUserId={sessionUserId}
expense={expense}
/>
))}
{/* {group.members.map((member) => (
<li key={member.id}>
<UserCard className="border bg-background" user={member.user!}>
<span className="text-xs text-muted-foreground">
{member.role}
</span>
<RemoveFromGroupButton
userId={member.user.id}
groupId={group.id}
/>
</UserCard>
</li>
))} */}
</ul>
</Section>
</>

View File

@ -4,12 +4,12 @@ import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: appConfig.name,
short_name: "NextPWA",
description: "A Progressive Web App built with Next.js",
short_name: appConfig.shortName,
description: "Split your expenses with friends",
start_url: "/",
display: "standalone",
background_color: "#ffffff",
theme_color: "#000000",
background_color: "#000000",
theme_color: "#ffffff",
icons: [
{
src: "/icon-192x192.png",

View File

@ -0,0 +1,12 @@
"use client";
import { useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { useExpenseStore } from "../store/expense-store";
export default function useExpenseParticipantsParams() {
const searchParams = useSearchParams();
const addParticipants = useExpenseStore((state) => state.addParticipants);
// groupId =nmiqj2qgbmi923rqpgu4ngod;
useEffect(() => {}, []);
}