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 = {
|
type AppConfig = {
|
||||||
name: string;
|
name: string;
|
||||||
|
shortName: string;
|
||||||
navigator: Array<NavLink>;
|
navigator: Array<NavLink>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appConfig: AppConfig = {
|
export const appConfig: AppConfig = {
|
||||||
name: "Bettesplit",
|
name: "Bettesplit",
|
||||||
|
shortName: "BS",
|
||||||
navigator: [
|
navigator: [
|
||||||
{
|
{
|
||||||
name: "Home",
|
name: "Home",
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import React from "react";
|
|||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const splits = await api.expense.getAll();
|
const splits = await api.expense.getAll();
|
||||||
const sessionUser = await currentUser();
|
const sessionUser = await currentUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header text="Expenses">
|
<Header text="Expenses">
|
||||||
@ -20,26 +21,6 @@ export default async function Page() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Header>
|
</Header>
|
||||||
<Section>
|
<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 */}
|
{/* FILTER BAR */}
|
||||||
<div className="py-4 flex items-center gap-2">
|
<div className="py-4 flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import GroupPage from "@/app/_components/group/group-page";
|
import GroupPage from "@/app/_components/group/group-page";
|
||||||
import { api, HydrateClient } from "@/trpc/server";
|
import { api, HydrateClient } from "@/trpc/server";
|
||||||
|
import { currentUser } from "@clerk/nextjs/server";
|
||||||
|
|
||||||
async function Page({
|
async function Page({
|
||||||
params,
|
params,
|
||||||
@ -11,10 +12,11 @@ async function Page({
|
|||||||
}) {
|
}) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
void api.group.get.prefetch({ id });
|
void api.group.get.prefetch({ id });
|
||||||
|
const sessionUser = await currentUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HydrateClient>
|
<HydrateClient>
|
||||||
<GroupPage groupId={id} />
|
<GroupPage sessionUserId={sessionUser?.id!} groupId={id} />
|
||||||
</HydrateClient>
|
</HydrateClient>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,11 +17,11 @@ export default async function Home() {
|
|||||||
<Header
|
<Header
|
||||||
text={
|
text={
|
||||||
<>
|
<>
|
||||||
<Icons.logo
|
{/* <Icons.logo
|
||||||
className="size-32 scale-125 mr-2 absolute -left-4 -top-1/2 text-muted-foreground/5 -z-10
|
className="size-32 scale-125 mr-2 absolute -left-4 -top-1/2 text-muted-foreground/5 -z-10
|
||||||
mask-r-from-30%
|
mask-r-from-30%
|
||||||
"
|
"
|
||||||
/>
|
/> */}
|
||||||
<span>{appConfig.name}</span>
|
<span>{appConfig.name}</span>
|
||||||
<ModeToggle className="ml-4" />
|
<ModeToggle className="ml-4" />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -21,23 +21,8 @@ function ExpensePage({ sessionUser }: { sessionUser: User }) {
|
|||||||
|
|
||||||
const groupBadges = useExpenseStore((state) => state.groupBadges);
|
const groupBadges = useExpenseStore((state) => state.groupBadges);
|
||||||
const addGroupBadges = useExpenseStore((state) => state.addGroupBadges);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header text="Add Expense">
|
<Header text="Add Expense">
|
||||||
|
|||||||
@ -1,22 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React from "react";
|
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 PaidByInput from "./paid-by";
|
||||||
import { api } from "@/trpc/react";
|
import SplitBetweenInput from "./split-between-input";
|
||||||
import SplitTo from "./split-to";
|
|
||||||
import type { User } from "@/server/db/schema";
|
import type { User } from "@/server/db/schema";
|
||||||
|
|
||||||
export default function ExpenseSplit({ sessionUser }: { sessionUser: User }) {
|
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 (
|
return (
|
||||||
<div className="space-y-4 ">
|
<div className="space-y-4 ">
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
@ -25,81 +13,8 @@ export default function ExpenseSplit({ sessionUser }: { sessionUser: User }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<span>and spliited</span>
|
<span>and spliited</span>
|
||||||
<SplitTo />
|
<SplitBetweenInput />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,10 +10,8 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { DialogClose } from "@radix-ui/react-dialog";
|
import { DialogClose } from "@radix-ui/react-dialog";
|
||||||
import type { PublicUser } from "next-auth";
|
|
||||||
import Avatar from "@/components/avatar";
|
import Avatar from "@/components/avatar";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { NumberInput } from "@/components/number-input";
|
import { NumberInput } from "@/components/number-input";
|
||||||
import { EuroIcon } from "lucide-react";
|
import { EuroIcon } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|||||||
@ -10,8 +10,11 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useExpenseStore } from "@/lib/store/expense-store";
|
import { useExpenseStore } from "@/lib/store/expense-store";
|
||||||
function SplitTo() {
|
|
||||||
|
function SplitBetweenInput() {
|
||||||
|
const participants = useExpenseStore((state) => state.participants);
|
||||||
const splitType = useExpenseStore((state) => state.splitType);
|
const splitType = useExpenseStore((state) => state.splitType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger className="flex items-center flex-wrap gap-2" asChild>
|
<DialogTrigger className="flex items-center flex-wrap gap-2" asChild>
|
||||||
@ -25,7 +28,7 @@ function SplitTo() {
|
|||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<div className="flex items-center mb-8">
|
<div className="flex items-center mb-8">
|
||||||
<DialogTitle className="text-2xl">Who paid?</DialogTitle>
|
<DialogTitle className="text-2xl">Split between</DialogTitle>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button className="ml-auto">Go Back</Button>
|
<Button className="ml-auto">Go Back</Button>
|
||||||
</DialogClose>
|
</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" />
|
<Icons.group className="size-4" />
|
||||||
<span>{group.name}</span>
|
<span>{group.name}</span>
|
||||||
</CardTitle>
|
</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>
|
</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>
|
<CardDescription>
|
||||||
<p>{group.description}</p>
|
<p>{group.description}</p>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
|||||||
@ -13,10 +13,15 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Icons } from "@/components/icons";
|
import { Icons } from "@/components/icons";
|
||||||
import ExpenseCard from "../expense/expense-card";
|
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 });
|
const [group] = api.group.get.useSuspenseQuery({ id: groupId });
|
||||||
if (!group) return <GroupFormDrawer />;
|
if (!group) return <GroupFormDrawer />;
|
||||||
|
|
||||||
const groupExpenses = group?.groupBadges?.map(({ expense }) => expense);
|
const groupExpenses = group?.groupBadges?.map(({ expense }) => expense);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -63,21 +68,12 @@ function GroupPage({ groupId }: { groupId: string }) {
|
|||||||
|
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{groupExpenses.map((expense) => (
|
{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>
|
</ul>
|
||||||
</Section>
|
</Section>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -4,12 +4,12 @@ import type { MetadataRoute } from "next";
|
|||||||
export default function manifest(): MetadataRoute.Manifest {
|
export default function manifest(): MetadataRoute.Manifest {
|
||||||
return {
|
return {
|
||||||
name: appConfig.name,
|
name: appConfig.name,
|
||||||
short_name: "NextPWA",
|
short_name: appConfig.shortName,
|
||||||
description: "A Progressive Web App built with Next.js",
|
description: "Split your expenses with friends",
|
||||||
start_url: "/",
|
start_url: "/",
|
||||||
display: "standalone",
|
display: "standalone",
|
||||||
background_color: "#ffffff",
|
background_color: "#000000",
|
||||||
theme_color: "#000000",
|
theme_color: "#ffffff",
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: "/icon-192x192.png",
|
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