production test
This commit is contained in:
parent
76f1685c71
commit
a945005e76
15
Dockerfile
Normal file
15
Dockerfile
Normal 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"]
|
||||
@ -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
39
docker-compose.yml
Normal 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
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
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
@ -1,10 +1,12 @@
|
||||
type AppConfig = {
|
||||
name: string;
|
||||
shortName: string;
|
||||
navigator: Array<NavLink>;
|
||||
};
|
||||
|
||||
export const appConfig: AppConfig = {
|
||||
name: "Bettesplit",
|
||||
shortName: "BS",
|
||||
navigator: [
|
||||
{
|
||||
name: "Home",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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" />
|
||||
</>
|
||||
|
||||
@ -22,22 +22,7 @@ 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">
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
|
||||
@ -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",
|
||||
|
||||
12
src/lib/hooks/use-expense-params.tsx
Normal file
12
src/lib/hooks/use-expense-params.tsx
Normal 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(() => {}, []);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user