From 85cfb319b0477263daa51d996eef91ab295c665e Mon Sep 17 00:00:00 2001 From: nzambello Date: Sun, 13 Feb 2022 21:25:00 +0100 Subject: [PATCH] feat: working on ui --- app/components/Header.tsx | 101 ++++++ app/icons/ChartSquare.tsx | 44 +++ app/icons/Filter.tsx | 28 ++ app/icons/Group.tsx | 41 +++ app/icons/GroupAlt.tsx | 57 +++ app/icons/Login.tsx | 33 ++ app/icons/Logout.tsx | 33 ++ app/icons/MoneyAdd.tsx | 61 ++++ app/icons/People.tsx | 55 +++ app/icons/Percentage.tsx | 41 +++ app/icons/WalletSeach.tsx | 47 +++ app/root.tsx | 5 +- app/routes/account.tsx | 0 app/routes/expenses.tsx | 83 ++--- app/routes/expenses/index.tsx | 85 ++++- app/routes/expenses/list.tsx | 0 app/routes/index.tsx | 34 +- app/routes/login.tsx | 331 ++++++++---------- app/routes/signin.tsx | 267 ++++++++++++++ app/routes/statistics.tsx | 0 app/routes/team.tsx | 0 app/utils/session.server.ts | 18 +- .../20220213201936_add_teamid/migration.sql | 23 ++ prisma/schema.prisma | 1 + prisma/seed.ts | 6 + 25 files changed, 1121 insertions(+), 273 deletions(-) create mode 100644 app/components/Header.tsx create mode 100644 app/icons/ChartSquare.tsx create mode 100644 app/icons/Filter.tsx create mode 100644 app/icons/Group.tsx create mode 100644 app/icons/GroupAlt.tsx create mode 100644 app/icons/Login.tsx create mode 100644 app/icons/Logout.tsx create mode 100644 app/icons/MoneyAdd.tsx create mode 100644 app/icons/People.tsx create mode 100644 app/icons/Percentage.tsx create mode 100644 app/icons/WalletSeach.tsx create mode 100644 app/routes/account.tsx create mode 100644 app/routes/expenses/list.tsx create mode 100644 app/routes/signin.tsx create mode 100644 app/routes/statistics.tsx create mode 100644 app/routes/team.tsx create mode 100644 prisma/migrations/20220213201936_add_teamid/migration.sql diff --git a/app/components/Header.tsx b/app/components/Header.tsx new file mode 100644 index 0000000..db8b35e --- /dev/null +++ b/app/components/Header.tsx @@ -0,0 +1,101 @@ +import type { User } from "@prisma/client"; +import { Link, Form } from "remix"; +import People from "~/icons/People"; +import MoneyAdd from "~/icons/MoneyAdd"; +import LoginSVG from "../icons/Login"; +import Percentage from "~/icons/Percentage"; + +interface Props { + user?: User | null; + route?: string; +} + +const Header = ({ user, route }: Props) => { + return ( +
+ +
+ ); +}; + +export default Header; diff --git a/app/icons/ChartSquare.tsx b/app/icons/ChartSquare.tsx new file mode 100644 index 0000000..098d31b --- /dev/null +++ b/app/icons/ChartSquare.tsx @@ -0,0 +1,44 @@ +const ChartSquare = ({ className }: { className?: string }) => ( + + + + + + +); + +export default ChartSquare; diff --git a/app/icons/Filter.tsx b/app/icons/Filter.tsx new file mode 100644 index 0000000..62a9efa --- /dev/null +++ b/app/icons/Filter.tsx @@ -0,0 +1,28 @@ +const Filter = () => ( + + + + +); + +export default Filter; diff --git a/app/icons/Group.tsx b/app/icons/Group.tsx new file mode 100644 index 0000000..9203b4e --- /dev/null +++ b/app/icons/Group.tsx @@ -0,0 +1,41 @@ +const Group = ({ className }: { className: string }) => ( + + + + + + +); + +export default Group; diff --git a/app/icons/GroupAlt.tsx b/app/icons/GroupAlt.tsx new file mode 100644 index 0000000..9d2251e --- /dev/null +++ b/app/icons/GroupAlt.tsx @@ -0,0 +1,57 @@ +const GroupAlt = ({ className }: { className: string }) => ( + + + + + + + + +); + +export default GroupAlt; diff --git a/app/icons/Login.tsx b/app/icons/Login.tsx new file mode 100644 index 0000000..bc0250d --- /dev/null +++ b/app/icons/Login.tsx @@ -0,0 +1,33 @@ +const Login = () => ( + + + + + +); + +export default Login; diff --git a/app/icons/Logout.tsx b/app/icons/Logout.tsx new file mode 100644 index 0000000..e5038af --- /dev/null +++ b/app/icons/Logout.tsx @@ -0,0 +1,33 @@ +const Logout = () => ( + + + + + +); + +export default Logout; diff --git a/app/icons/MoneyAdd.tsx b/app/icons/MoneyAdd.tsx new file mode 100644 index 0000000..deb6960 --- /dev/null +++ b/app/icons/MoneyAdd.tsx @@ -0,0 +1,61 @@ +const MoneyAdd = ({ className }: { className: string }) => ( + + + + + + + + +); + +export default MoneyAdd; diff --git a/app/icons/People.tsx b/app/icons/People.tsx new file mode 100644 index 0000000..4fe2c6b --- /dev/null +++ b/app/icons/People.tsx @@ -0,0 +1,55 @@ +const People = ({ className }: { className: string }) => ( + + + + + + + + +); + +export default People; diff --git a/app/icons/Percentage.tsx b/app/icons/Percentage.tsx new file mode 100644 index 0000000..bfa224f --- /dev/null +++ b/app/icons/Percentage.tsx @@ -0,0 +1,41 @@ +const Percentage = ({ className }: { className: string }) => ( + + + + + + +); + +export default Percentage; diff --git a/app/icons/WalletSeach.tsx b/app/icons/WalletSeach.tsx new file mode 100644 index 0000000..88c6cff --- /dev/null +++ b/app/icons/WalletSeach.tsx @@ -0,0 +1,47 @@ +const WalletSeach = () => ( + + + + + + + +); + +export default WalletSeach; diff --git a/app/root.tsx b/app/root.tsx index 55f8f18..ce638ed 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,5 +1,6 @@ import type { LinksFunction, MetaFunction } from "remix"; import { Links, LiveReload, Outlet, useCatch, Meta, Scripts } from "remix"; +import Header from "./components/Header"; import styles from "./tailwind.css"; @@ -44,14 +45,14 @@ function Document({ title?: string; }) { return ( - + {title} - + {children} {process.env.NODE_ENV === "development" ? : null} diff --git a/app/routes/account.tsx b/app/routes/account.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/expenses.tsx b/app/routes/expenses.tsx index 27232e1..6fd393e 100644 --- a/app/routes/expenses.tsx +++ b/app/routes/expenses.tsx @@ -1,31 +1,24 @@ -import type { Expense, User } from "@prisma/client"; +import type { User, Team } from "@prisma/client"; import type { LinksFunction, LoaderFunction } from "remix"; -import { Link, Outlet, useLoaderData, Form, redirect } from "remix"; -import { db } from "~/utils/db.server"; +import { Outlet, useLoaderData, Form, redirect, useCatch } from "remix"; import { getUser } from "~/utils/session.server"; +import Header from "../components/Header"; export const links: LinksFunction = () => { return []; }; type LoaderData = { - user: User | null; - expenseListItems: Array; + user: (User & { team: Team & { members: User[] } }) | null; }; export const loader: LoaderFunction = async ({ request }) => { const user = await getUser(request); - if (!user) { - redirect("/login"); + if (!user?.id) { + return redirect("/login"); } - const expenseListItems = await db.expense.findMany({ - take: 25, - orderBy: { createdAt: "desc" }, - }); - const data: LoaderData = { - expenseListItems, user, }; return data; @@ -35,47 +28,29 @@ export default function ExpensesRoute() { const data = useLoaderData(); return ( -
-
-
-

- - Expenses - -

- {data.user ? ( -
- {`Hi ${data.user.username}`} -
- -
-
- ) : ( - Login - )} -
-
-
-
-
-

Last expenses:

-
    - {data.expenseListItems.map((exp) => ( -
  • - - {exp.amount}€ - {exp.description} - -
  • - ))} -
-
-
- -
-
+ <> +
+
+
-
+ ); } + +export function CatchBoundary() { + const caught = useCatch(); + + if (caught.status === 401) { + return redirect("/login"); + } + if (caught.status === 404) { + return ( +
There are no expenses to display.
+ ); + } + throw new Error(`Unexpected caught response with status: ${caught.status}`); +} + +export function ErrorBoundary() { + return
I did a whoopsies.
; +} diff --git a/app/routes/expenses/index.tsx b/app/routes/expenses/index.tsx index baf8a69..f4cebd9 100644 --- a/app/routes/expenses/index.tsx +++ b/app/routes/expenses/index.tsx @@ -1,17 +1,27 @@ import type { LoaderFunction } from "remix"; -import { useLoaderData, Link, useCatch } from "remix"; -import type { Expense } from "@prisma/client"; +import { useLoaderData, Link, useCatch, redirect } from "remix"; +import type { Expense, User } from "@prisma/client"; import { db } from "~/utils/db.server"; +import { getUser } from "~/utils/session.server"; +import Group from "~/icons/Group"; -type LoaderData = { lastExpenses: Expense[] }; +type LoaderData = { lastExpenses: (Expense & { user: User })[]; user: User }; -export const loader: LoaderFunction = async () => { +export const loader: LoaderFunction = async ({ request }) => { + const user = await getUser(request); + if (!user) { + return redirect("/login"); + } const lastExpenses = await db.expense.findMany({ + include: { + user: true, + }, take: 25, orderBy: { createdAt: "desc" }, + where: { teamId: user.teamId }, }); - const data: LoaderData = { lastExpenses }; + const data: LoaderData = { lastExpenses, user }; return data; }; @@ -19,12 +29,65 @@ export default function JokesIndexRoute() { const data = useLoaderData(); return ( -
-

Here show statistics

- - - Add an expense - +
+
+
+

Last expenses

+ {data.lastExpenses?.map((exp) => ( +
+
+ {exp.user.icon ?? exp.user.username[0]} +
+
+ 0 ? "text-error" : "text-success" + }`} + > + {-exp.amount} € + +
+
{exp.description}
+
+ ))} + + See all + +
+
+
+
+

Who needs to pay who

+
+
+
+
+ + + + + Add an expense + +
+
+
+
+ + + Trasfer + +
+
); } diff --git a/app/routes/expenses/list.tsx b/app/routes/expenses/list.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/index.tsx b/app/routes/index.tsx index 832a181..631e26b 100644 --- a/app/routes/index.tsx +++ b/app/routes/index.tsx @@ -1,8 +1,10 @@ +import type { User } from "@prisma/client"; import type { LinksFunction, MetaFunction, LoaderFunction } from "remix"; import { Link, useLoaderData } from "remix"; -import { getUserId } from "~/utils/session.server"; +import Header from "~/components/Header"; +import { getUser } from "~/utils/session.server"; -type LoaderData = { userId: string | null }; +type LoaderData = { user: User | null }; export const links: LinksFunction = () => { return []; @@ -17,8 +19,8 @@ export const meta: MetaFunction = () => { }; export const loader: LoaderFunction = async ({ request }) => { - const userId = await getUserId(request); - const data: LoaderData = { userId }; + const user = await getUser(request); + const data: LoaderData = { user }; return data; }; @@ -26,23 +28,13 @@ export default function Index() { const data = useLoaderData(); return ( -
-
-

Explit

- + <> +
+
+
+

Explit

+
-
+ ); } diff --git a/app/routes/login.tsx b/app/routes/login.tsx index ed98832..b8b0b11 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -2,6 +2,7 @@ import type { ActionFunction, LinksFunction, MetaFunction } from "remix"; import { useActionData, json, Link, useSearchParams, Form } from "remix"; import { db } from "~/utils/db.server"; import { login, createUserSession, register } from "~/utils/session.server"; +import Header from "../components/Header"; export const links: LinksFunction = () => { return []; @@ -37,14 +38,10 @@ type ActionData = { fieldErrors?: { username: string | undefined; password: string | undefined; - teamId: string | undefined; }; fields?: { - loginType: string; username: string; password: string; - teamId: string; - icon?: string; }; }; @@ -52,18 +49,12 @@ const badRequest = (data: ActionData) => json(data, { status: 400 }); export const action: ActionFunction = async ({ request }) => { const form = await request.formData(); - const loginType = form.get("loginType"); const username = form.get("username"); const password = form.get("password"); - const icon = form.get("icon"); - const teamId = form.get("teamId"); const redirectTo = form.get("redirectTo") || "/expenses"; if ( - typeof loginType !== "string" || typeof username !== "string" || typeof password !== "string" || - typeof icon !== "string" || - typeof teamId !== "string" || typeof redirectTo !== "string" ) { return badRequest({ @@ -71,53 +62,22 @@ export const action: ActionFunction = async ({ request }) => { }); } - const fields = { loginType, username, password, teamId }; + const fields = { username, password }; const fieldErrors = { username: validateUsername(username), password: validatePassword(password), - teamId: validateTeamId(teamId), }; if (Object.values(fieldErrors).some(Boolean)) return badRequest({ fieldErrors, fields }); - switch (loginType) { - case "login": { - const user = await login({ username, password, icon, teamId }); - if (!user) { - return badRequest({ - fields, - formError: `Username/Password combination is incorrect`, - }); - } - return createUserSession(user.id, redirectTo); - } - case "register": { - const userExists = await db.user.findFirst({ - where: { username }, - }); - if (userExists) { - console.error(userExists); - return badRequest({ - fields, - formError: `User with username ${username} already exists`, - }); - } - const user = await register({ username, password, icon, teamId }); - if (!user) { - return badRequest({ - fields, - formError: `Something went wrong trying to create a new user.`, - }); - } - return createUserSession(user.id, redirectTo); - } - default: { - return badRequest({ - fields, - formError: `Login type invalid`, - }); - } + const user = await login({ username, password }); + if (!user) { + return badRequest({ + fields, + formError: `Username/Password combination is incorrect`, + }); } + return createUserSession(user.id, redirectTo); }; export default function Login() { @@ -125,143 +85,150 @@ export default function Login() { const [searchParams] = useSearchParams(); return ( -
-
-

Login

-
- -
- Login or Register? -
-
- - - {actionData?.fieldErrors?.username ? ( -
+ + + {actionData?.fieldErrors?.username && ( +
+
+ + + + +
+
+ )} +
+
+ + + {actionData?.fieldErrors?.password && ( +
+
+ + + + +
+
+ )} +
+
+ {actionData?.formError && ( +
+
+ + + + +
+
+ )} +
+ +
-
- - - {actionData?.fieldErrors?.teamId ? ( - - ) : null} -
-
- - -
-
- - - {actionData?.fieldErrors?.password ? ( - - ) : null} -
-
- {actionData?.formError ? ( -

- {actionData?.formError} -

- ) : null} -
- - +
-
-
    -
  • - Home -
  • -
+
+
+
    +
  • + Home +
  • +
  • + Sign-in +
  • +
+
-
+ ); } diff --git a/app/routes/signin.tsx b/app/routes/signin.tsx new file mode 100644 index 0000000..a88a115 --- /dev/null +++ b/app/routes/signin.tsx @@ -0,0 +1,267 @@ +import type { ActionFunction, LinksFunction, MetaFunction } from "remix"; +import { useActionData, json, Link, useSearchParams, Form } from "remix"; +import { db } from "~/utils/db.server"; +import { login, createUserSession, register } from "~/utils/session.server"; + +export const links: LinksFunction = () => { + return []; +}; + +export const meta: MetaFunction = () => { + return { + title: "Explit | Login", + description: "Login to track and split your expenses!", + }; +}; + +function validateUsername(username: unknown) { + if (typeof username !== "string" || username.length < 3) { + return `Usernames must be at least 3 characters long`; + } +} + +function validatePassword(password: unknown) { + if (typeof password !== "string" || password.length < 6) { + return `Passwords must be at least 6 characters long`; + } +} + +function validateTeamId(teamId: unknown) { + if (typeof teamId !== "string" || teamId.length < 1) { + return "You must indicate an arbitrary team ID"; + } +} + +type ActionData = { + formError?: string; + fieldErrors?: { + username: string | undefined; + password: string | undefined; + }; + fields?: { + loginType: string; + username: string; + password: string; + teamId?: string; + icon?: string; + }; +}; + +const badRequest = (data: ActionData) => json(data, { status: 400 }); + +export const action: ActionFunction = async ({ request }) => { + const form = await request.formData(); + const loginType = form.get("loginType"); + const username = form.get("username"); + const password = form.get("password"); + const icon = form.get("icon"); + const teamId = form.get("teamId"); + const redirectTo = form.get("redirectTo") || "/expenses"; + if ( + typeof loginType !== "string" || + typeof username !== "string" || + typeof password !== "string" || + (loginType === "register" && + (typeof icon !== "string" || + typeof teamId !== "string" || + typeof redirectTo !== "string")) + ) { + return badRequest({ + formError: `Form not submitted correctly.`, + }); + } + + const fields = { loginType, username, password, teamId }; + const fieldErrors = { + username: validateUsername(username), + password: validatePassword(password), + teamId: loginType === "register" && validateTeamId(teamId), + }; + if (Object.values(fieldErrors).some(Boolean)) + return badRequest({ fieldErrors, fields }); + + switch (loginType) { + case "login": { + const user = await login({ username, password }); + if (!user) { + return badRequest({ + fields, + formError: `Username/Password combination is incorrect`, + }); + } + return createUserSession(user.id, redirectTo); + } + case "register": { + const userExists = await db.user.findFirst({ + where: { username }, + }); + if (userExists) { + console.error(userExists); + return badRequest({ + fields, + formError: `User with username ${username} already exists`, + }); + } + const user = await register({ username, password, icon, teamId }); + if (!user) { + return badRequest({ + fields, + formError: `Something went wrong trying to create a new user.`, + }); + } + return createUserSession(user.id, redirectTo); + } + default: { + return badRequest({ + fields, + formError: `Login type invalid`, + }); + } + } +}; + +export default function Login() { + const actionData = useActionData(); + const [searchParams] = useSearchParams(); + + return ( +
+
+

Login

+
+ +
+ Login or Register? + + +
+
+ + + {actionData?.fieldErrors?.username ? ( + + ) : null} +
+
+ + + {actionData?.fieldErrors?.teamId ? ( + + ) : null} +
+
+ + +
+
+ + + {actionData?.fieldErrors?.password ? ( + + ) : null} +
+
+ {actionData?.formError ? ( +

+ {actionData?.formError} +

+ ) : null} +
+ +
+
+
+
    +
  • + Home +
  • +
+
+
+ ); +} diff --git a/app/routes/statistics.tsx b/app/routes/statistics.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/team.tsx b/app/routes/team.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/session.server.ts b/app/utils/session.server.ts index 53c9459..2cb4648 100644 --- a/app/utils/session.server.ts +++ b/app/utils/session.server.ts @@ -5,7 +5,12 @@ import { db } from "./db.server"; type LoginForm = { username: string; password: string; - icon?: string; +}; + +type RegisterForm = { + username: string; + password: string; + icon: string; teamId: string; }; @@ -14,7 +19,7 @@ export async function register({ password, icon, teamId, -}: LoginForm) { +}: RegisterForm) { const passwordHash = await bcrypt.hash(password, 10); const team = await db.team.findUnique({ where: { id: teamId } }); if (!team) { @@ -94,6 +99,13 @@ export async function getUser(request: Request) { try { const user = await db.user.findUnique({ where: { id: userId }, + include: { + team: { + include: { + members: true, + }, + }, + }, }); return user; } catch { @@ -103,7 +115,7 @@ export async function getUser(request: Request) { export async function logout(request: Request) { const session = await storage.getSession(request.headers.get("Cookie")); - return redirect("/login", { + return redirect("/", { headers: { "Set-Cookie": await storage.destroySession(session), }, diff --git a/prisma/migrations/20220213201936_add_teamid/migration.sql b/prisma/migrations/20220213201936_add_teamid/migration.sql new file mode 100644 index 0000000..497910e --- /dev/null +++ b/prisma/migrations/20220213201936_add_teamid/migration.sql @@ -0,0 +1,23 @@ +/* + Warnings: + + - Added the required column `teamId` to the `Expense` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Expense" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "teamId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "description" TEXT NOT NULL, + "amount" REAL NOT NULL, + CONSTRAINT "Expense_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_Expense" ("amount", "createdAt", "description", "id", "updatedAt", "userId") SELECT "amount", "createdAt", "description", "id", "updatedAt", "userId" FROM "Expense"; +DROP TABLE "Expense"; +ALTER TABLE "new_Expense" RENAME TO "Expense"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 29df2a1..0556679 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -33,6 +33,7 @@ model Expense { id String @id @default(uuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) + teamId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt description String diff --git a/prisma/seed.ts b/prisma/seed.ts index dd8a0bb..964ae91 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -33,16 +33,19 @@ async function seed() { description: "Spesa", amount: 100, userId: nicola.id, + teamId: famiglia.id, }, { description: "Spesa", amount: 70, userId: shahra.id, + teamId: famiglia.id, }, { description: "Affitto", amount: 500, userId: shahra.id, + teamId: famiglia.id, }, // transaction between users @@ -50,17 +53,20 @@ async function seed() { description: "Affitto", amount: 250, userId: nicola.id, + teamId: famiglia.id, }, { description: "Affitto", amount: -250, userId: shahra.id, + teamId: famiglia.id, }, { description: "Cena", amount: 50, userId: nicola.id, + teamId: famiglia.id, }, ];