From 88e11ba3a1ee8fa1b255c10391e632b7f5abd3dd Mon Sep 17 00:00:00 2001 From: LobeTia Date: Fri, 12 Apr 2024 02:20:44 +0200 Subject: [PATCH 1/5] Progress --- package-lock.json | 32 + package.json | 1 + .../migration.sql | 22 + prisma/schema.prisma | 45 +- .../@authenticated/membership/layout.tsx | 35 + .../manage/@modal/(.)create/[id]/page.tsx | 9 + .../manage/@modal/(..)create/[id]/page.tsx | 9 + .../manage/@modal/create/[id]/page.tsx | 7 + .../membership/manage/@modal/default.tsx | 3 + .../membership/manage/@modal/page.tsx | 3 + .../membership/manage/default.tsx | 3 + .../membership/manage/layout.tsx | 14 + .../@authenticated/membership/manage/page.tsx | 73 ++ .../admin/@authenticated/membership/page.tsx | 4 +- .../membership/pending-review/page.tsx | 11 + .../components/discordSignIn.tsx | 31 +- src/app/admin/const.ts | 17 + src/app/devPage.tsx | 3 +- src/app/members/@authenticated/page.tsx | 2 +- src/app/page.tsx | 10 +- src/app/signup/page.tsx | 674 +++++++++--------- src/components/devtool/debug.tsx | 2 +- .../molecules}/membershipCard.tsx | 0 src/components/molecules/modal.tsx | 31 + src/components/ui/button.tsx | 1 + src/components/ui/dialog.tsx | 122 ++++ src/components/ui/table.tsx | 120 ++++ 27 files changed, 909 insertions(+), 375 deletions(-) create mode 100644 prisma/migrations/20240411161725_add_membership_template/migration.sql create mode 100644 src/app/admin/@authenticated/membership/layout.tsx create mode 100644 src/app/admin/@authenticated/membership/manage/@modal/(.)create/[id]/page.tsx create mode 100644 src/app/admin/@authenticated/membership/manage/@modal/(..)create/[id]/page.tsx create mode 100644 src/app/admin/@authenticated/membership/manage/@modal/create/[id]/page.tsx create mode 100644 src/app/admin/@authenticated/membership/manage/@modal/default.tsx create mode 100644 src/app/admin/@authenticated/membership/manage/@modal/page.tsx create mode 100644 src/app/admin/@authenticated/membership/manage/default.tsx create mode 100644 src/app/admin/@authenticated/membership/manage/layout.tsx create mode 100644 src/app/admin/@authenticated/membership/manage/page.tsx create mode 100644 src/app/admin/@authenticated/membership/pending-review/page.tsx rename src/{app/signup/components => components/molecules}/membershipCard.tsx (100%) create mode 100644 src/components/molecules/modal.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/table.tsx diff --git a/package-lock.json b/package-lock.json index 0cac1ed..ba239c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@stripe/react-stripe-js": "^2.6.2", "@stripe/stripe-js": "^3.2.0", "@t3-oss/env-nextjs": "^0.9.2", + "@tanstack/react-table": "^8.15.3", "@types/mdx": "^2.0.12", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", @@ -2632,6 +2633,37 @@ } } }, + "node_modules/@tanstack/react-table": { + "version": "8.15.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.15.3.tgz", + "integrity": "sha512-aocQ4WpWiAh7R+yxNp+DGQYXeVACh5lv2kk96DjYgFiHDCB0cOFoYMT/pM6eDOzeMXR9AvPoLeumTgq8/0qX+w==", + "dependencies": { + "@tanstack/table-core": "8.15.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.15.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.15.3.tgz", + "integrity": "sha512-wOgV0HfEvuMOv8RlqdR9MdNNqq0uyvQtP39QOvGlggHvIObOE4exS+D5LGO8LZ3LUXxId2IlUKcHDHaGujWhUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/acorn": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", diff --git a/package.json b/package.json index 0127c33..3781c41 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@stripe/react-stripe-js": "^2.6.2", "@stripe/stripe-js": "^3.2.0", "@t3-oss/env-nextjs": "^0.9.2", + "@tanstack/react-table": "^8.15.3", "@types/mdx": "^2.0.12", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", diff --git a/prisma/migrations/20240411161725_add_membership_template/migration.sql b/prisma/migrations/20240411161725_add_membership_template/migration.sql new file mode 100644 index 0000000..8daa0e4 --- /dev/null +++ b/prisma/migrations/20240411161725_add_membership_template/migration.sql @@ -0,0 +1,22 @@ +-- CreateEnum +CREATE TYPE "PricePeriod" AS ENUM ('Yearly', 'Monthly'); + +-- CreateEnum +CREATE TYPE "PriceUnit" AS ENUM ('EUR', 'USD'); + +-- CreateTable +CREATE TABLE "MembershipTemplate" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "features" TEXT[], + "priceAmount" INTEGER NOT NULL, + "pricePeriod" "PricePeriod" NOT NULL DEFAULT 'Yearly', + "priceUnit" "PriceUnit" NOT NULL DEFAULT 'EUR', + "stripePriceId" TEXT NOT NULL, + + CONSTRAINT "MembershipTemplate_pkey" PRIMARY KEY ("id") +); + +ALTER TABLE "MembershipTemplate" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 43cd773..8bf99ff 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -61,19 +61,19 @@ model Session { } model User { - id String @id @default(cuid()) - name String? - role UserRole @default(member) - email String? @unique - emailVerified DateTime? - image String? - stripeCustomerId String? - accounts Account[] - sessions Session[] - memberships Membership[] + id String @id @default(cuid()) + name String? + role UserRole @default(member) + email String? @unique + emailVerified DateTime? + image String? + stripeCustomerId String? + accounts Account[] + sessions Session[] + memberships Membership[] } -enum UserRole{ +enum UserRole { member admin } @@ -107,3 +107,26 @@ model Membership { stripeSubscriptionId String? @unique // @db.Text user User @relation(fields: [userId], references: [id], onDelete: Cascade) } + +model MembershipTemplate { + id String @id @default(cuid()) + title String + description String? + features String[] + priceAmount Int + pricePeriod PricePeriod @default(Yearly) + priceUnit PriceUnit @default(EUR) + stripePriceId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +enum PricePeriod { + Yearly + Monthly +} + +enum PriceUnit { + EUR + USD +} diff --git a/src/app/admin/@authenticated/membership/layout.tsx b/src/app/admin/@authenticated/membership/layout.tsx new file mode 100644 index 0000000..4bf6052 --- /dev/null +++ b/src/app/admin/@authenticated/membership/layout.tsx @@ -0,0 +1,35 @@ +import {find} from "lodash"; + +import {adminMenuTreeConfig} from "@/app/admin/const"; +import {LinkWithActive} from "@/components/molecules/linkWithActive"; + +interface AdminAuthenticatedLayoutInterface { + children: React.ReactNode; +} + +export default async function AdminAuthenticatedLayout({ + children, + }: AdminAuthenticatedLayoutInterface) { + const settingsChildren = find(adminMenuTreeConfig, {id: "membership"})!; + + return ( + <> +
+

Membership

+
+
+ +
{children}
+
+ + ); +} diff --git a/src/app/admin/@authenticated/membership/manage/@modal/(.)create/[id]/page.tsx b/src/app/admin/@authenticated/membership/manage/@modal/(.)create/[id]/page.tsx new file mode 100644 index 0000000..75cc3de --- /dev/null +++ b/src/app/admin/@authenticated/membership/manage/@modal/(.)create/[id]/page.tsx @@ -0,0 +1,9 @@ +import {Modal} from "@/components/molecules/modal"; + +export default function AdminMembershipModalCreatePage({ + params: {id}, + }: { + params: { id: string }; +}) { + return AdminMembershipModalCreatePage: {id} +} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/manage/@modal/(..)create/[id]/page.tsx b/src/app/admin/@authenticated/membership/manage/@modal/(..)create/[id]/page.tsx new file mode 100644 index 0000000..75cc3de --- /dev/null +++ b/src/app/admin/@authenticated/membership/manage/@modal/(..)create/[id]/page.tsx @@ -0,0 +1,9 @@ +import {Modal} from "@/components/molecules/modal"; + +export default function AdminMembershipModalCreatePage({ + params: {id}, + }: { + params: { id: string }; +}) { + return AdminMembershipModalCreatePage: {id} +} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/manage/@modal/create/[id]/page.tsx b/src/app/admin/@authenticated/membership/manage/@modal/create/[id]/page.tsx new file mode 100644 index 0000000..38a0359 --- /dev/null +++ b/src/app/admin/@authenticated/membership/manage/@modal/create/[id]/page.tsx @@ -0,0 +1,7 @@ +export default function AdminMembershipCreatePage({ + params: { id }, + }: { + params: { id: string }; +}) { + return
AdminMembershipCreatePage: {id}
; +} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/manage/@modal/default.tsx b/src/app/admin/@authenticated/membership/manage/@modal/default.tsx new file mode 100644 index 0000000..de43e6d --- /dev/null +++ b/src/app/admin/@authenticated/membership/manage/@modal/default.tsx @@ -0,0 +1,3 @@ +export default function AdminMembershipModalDefault() { + return null +} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/manage/@modal/page.tsx b/src/app/admin/@authenticated/membership/manage/@modal/page.tsx new file mode 100644 index 0000000..4f6b2c5 --- /dev/null +++ b/src/app/admin/@authenticated/membership/manage/@modal/page.tsx @@ -0,0 +1,3 @@ +export default function AdminMembershipModalPage(){ + return
PAGEEEE
+} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/manage/default.tsx b/src/app/admin/@authenticated/membership/manage/default.tsx new file mode 100644 index 0000000..de43e6d --- /dev/null +++ b/src/app/admin/@authenticated/membership/manage/default.tsx @@ -0,0 +1,3 @@ +export default function AdminMembershipModalDefault() { + return null +} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/manage/layout.tsx b/src/app/admin/@authenticated/membership/manage/layout.tsx new file mode 100644 index 0000000..f41142b --- /dev/null +++ b/src/app/admin/@authenticated/membership/manage/layout.tsx @@ -0,0 +1,14 @@ +export default function AdminMembershipLayout({ + modal, + children, + }: { + modal: React.ReactNode + children: React.ReactNode +}) { + return ( + <> +
{modal}
+
{children}
+ + ) +} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/manage/page.tsx b/src/app/admin/@authenticated/membership/manage/page.tsx new file mode 100644 index 0000000..ea681b3 --- /dev/null +++ b/src/app/admin/@authenticated/membership/manage/page.tsx @@ -0,0 +1,73 @@ +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@/components/ui/table"; +import {db} from "@/services/db"; +import {MembershipTemplate} from "@prisma/client"; +import {Button} from "@/components/ui/button"; +import {Pencil1Icon} from "@radix-ui/react-icons"; +import Link from "next/link"; + +async function getData() { + return db.membershipTemplate.findMany({ + orderBy: [ + { + id: 'desc' + } + ] + }) +} + +export default async function AdminMembershipPage() { + const membershipTemplates = await getData() + + return ( + <> +
+ + + +
+
+ + + + ID + Name + Price + Actions + + + + {membershipTemplates.map((membershipTemplate: MembershipTemplate) => ( + + {membershipTemplate.id} + {membershipTemplate.title} + + + + + + ))} + +
+
+ + ); +} + +function FormatPrice({element}: any) { + const moneyFormatter = new Intl.NumberFormat("en-US", { + currency: element.priceUnit, + minimumFractionDigits: 2, + style: "currency", + }); + + return {moneyFormatter.format(element.priceAmount / 100)} +} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/page.tsx b/src/app/admin/@authenticated/membership/page.tsx index 987a5f0..3b67788 100644 --- a/src/app/admin/@authenticated/membership/page.tsx +++ b/src/app/admin/@authenticated/membership/page.tsx @@ -1,8 +1,10 @@ +import {Paragraph} from "@/components/ui/typography"; + export default async function AdminMembershipPage() { return ( <>
-

Membership

+ Table with all the active membership
); diff --git a/src/app/admin/@authenticated/membership/pending-review/page.tsx b/src/app/admin/@authenticated/membership/pending-review/page.tsx new file mode 100644 index 0000000..39cb55d --- /dev/null +++ b/src/app/admin/@authenticated/membership/pending-review/page.tsx @@ -0,0 +1,11 @@ +import {Paragraph} from "@/components/ui/typography"; + +export default async function AdminMembershipPage() { + return ( + <> +
+ Table memberships that need reviews +
+ + ); +} diff --git a/src/app/admin/@unauthenticated/components/discordSignIn.tsx b/src/app/admin/@unauthenticated/components/discordSignIn.tsx index 8c1df1e..b6af32e 100644 --- a/src/app/admin/@unauthenticated/components/discordSignIn.tsx +++ b/src/app/admin/@unauthenticated/components/discordSignIn.tsx @@ -1,24 +1,19 @@ "use client"; import Image from "next/image"; -import { signIn } from "next-auth/react"; +import {signIn} from "next-auth/react"; +import {DiscordLogoIcon} from "@radix-ui/react-icons"; export function DiscordSignIn() { - return ( - - ); + return ( + + ); } diff --git a/src/app/admin/const.ts b/src/app/admin/const.ts index cf636d5..e03e177 100644 --- a/src/app/admin/const.ts +++ b/src/app/admin/const.ts @@ -10,6 +10,23 @@ export const adminMenuTreeConfig: MenuItem[] = [ id: "membership", label: "Membership", url: "/admin/membership", + children: [ + { + id: "membership-all", + label: "Active", + url: "/admin/membership", + }, + { + id: "membership-pending-review", + label: "Pending Review", + url: "/admin/membership/pending-review", + }, + { + id: "membership-manage", + label: "Manage", + url: "/admin/membership/manage", + }, + ], }, { id: "users", diff --git a/src/app/devPage.tsx b/src/app/devPage.tsx index 4866771..48ad890 100644 --- a/src/app/devPage.tsx +++ b/src/app/devPage.tsx @@ -4,6 +4,7 @@ import Link from "next/link"; import { Debug } from "@/components/devtool/debug"; import { Button } from "@/components/ui/button"; import { getServerAuthSession } from "@/server/auth"; +import {env} from "@/env"; export async function DevPage() { const session = await getServerAuthSession(); @@ -38,7 +39,7 @@ export async function DevPage() {
- {toPlainObject(process.env)} + {toPlainObject(env)}
diff --git a/src/app/members/@authenticated/page.tsx b/src/app/members/@authenticated/page.tsx index e7e992a..90cc822 100644 --- a/src/app/members/@authenticated/page.tsx +++ b/src/app/members/@authenticated/page.tsx @@ -2,7 +2,7 @@ import { MembershipCard, PricePeriod, PriceUnit, -} from "@/app/signup/components/membershipCard"; +} from "@/components/molecules/membershipCard"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { getServerAuthSession } from "@/server/auth"; diff --git a/src/app/page.tsx b/src/app/page.tsx index 7955111..53ae351 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,9 +1,9 @@ -import { redirect } from "next/navigation"; +import {redirect, RedirectType} from "next/navigation"; -import { DevPage } from "@/app/devPage"; -import { inDevEnvironment } from "@/lib/envs"; +import {DevPage} from "@/app/devPage"; +import {inDevEnvironment} from "@/lib/envs"; export default async function HomePage() { - if (inDevEnvironment) return ; - else redirect("/members"); + if (inDevEnvironment) return ; + else redirect("/members", RedirectType.replace); } diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index 3fee054..c36b2c5 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -1,381 +1,381 @@ "use client"; -import { zodResolver } from "@hookform/resolvers/zod"; +import {zodResolver} from "@hookform/resolvers/zod"; import { - Elements, - PaymentElement, - useElements, - useStripe, + Elements, + PaymentElement, + useElements, + useStripe, } from "@stripe/react-stripe-js"; -import { loadStripe } from "@stripe/stripe-js"; +import {loadStripe} from "@stripe/stripe-js"; import Image from "next/image"; import Link from "next/link"; -import { type Dispatch, type SetStateAction, useState } from "react"; -import { useFormState } from "react-dom"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; +import {type Dispatch, type SetStateAction, useState} from "react"; +import {useFormState} from "react-dom"; +import {useForm} from "react-hook-form"; +import {z} from "zod"; import { - createMembership, - type FormProps, + createMembership, + type FormProps, } from "@/app/actions/createMembership"; -import type { ServerActionState } from "@/app/actions/types"; -import { ServerActionStatus } from "@/app/actions/types"; +import type {ServerActionState} from "@/app/actions/types"; +import {ServerActionStatus} from "@/app/actions/types"; import { - MembershipCard, - PricePeriod, - PriceUnit, -} from "@/app/signup/components/membershipCard"; -import { StatefulButton } from "@/components/molecules/statefulButton"; -import { Button } from "@/components/ui/button"; -import { Checkbox } from "@/components/ui/checkbox"; + MembershipCard, + PricePeriod, + PriceUnit, +} from "@/components/molecules/membershipCard"; +import {StatefulButton} from "@/components/molecules/statefulButton"; +import {Button} from "@/components/ui/button"; +import {Checkbox} from "@/components/ui/checkbox"; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; +import {Input} from "@/components/ui/input"; import checkmark from "@/images/checkmark.svg"; +import { Debug } from "@/components/devtool/debug"; const formSchema = z.object({ - email: z.string().email(), - firstName: z.string().min(2, { - message: "Name must be at least 2 characters.", - }), - lastName: z.string().min(2, { - message: "Surname must be at least 2 characters.", - }), - socialSecurityNumber: z - .string() - .regex( - new RegExp( - /^[A-Za-z]{6}[0-9]{2}[A-Za-z]{1}[0-9]{2}[A-Za-z]{1}[0-9]{3}[A-Za-z]{1}$/, - ), - 'Invalid format, only italians "codice fiscale" are accepted', - ), - statuteApproval: z.boolean().refine((val) => val, { - message: "Please read and accept the statute", - }), + email: z.string().email(), + firstName: z.string().min(2, { + message: "Name must be at least 2 characters.", + }), + lastName: z.string().min(2, { + message: "Surname must be at least 2 characters.", + }), + socialSecurityNumber: z + .string() + .regex( + new RegExp( + /^[A-Za-z]{6}[0-9]{2}[A-Za-z]{1}[0-9]{2}[A-Za-z]{1}[0-9]{3}[A-Za-z]{1}$/, + ), + 'Invalid format, only italians "codice fiscale" are accepted', + ), + statuteApproval: z.boolean().refine((val) => val, { + message: "Please read and accept the statute", + }), }); -const stripePromise = loadStripe( - process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!, -); +const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!); export default function SignupPage() { - const [createMembershipState, createMembershipAction] = useFormState( - createMembership, - { - payload: {}, - status: ServerActionStatus.Pending, - }, - ); + const [createMembershipState, createMembershipAction] = useFormState( + createMembership, + { + payload: {}, + status: ServerActionStatus.Pending, + }, + ); - const form = useForm>({ - defaultValues: { - email: "", - firstName: "", - lastName: "", - socialSecurityNumber: "", - statuteApproval: true, - }, - resolver: zodResolver(formSchema), - }); + const form = useForm>({ + defaultValues: { + email: "", + firstName: "", + lastName: "", + socialSecurityNumber: "", + statuteApproval: false, + }, + resolver: zodResolver(formSchema), + }); - const handleForm = async (data: FormProps) => { - if (createMembershipState.nextStep === "confirmPayment") { - } else { - createMembershipAction(data); - } - }; - const [step, setStep] = useState(1); + const handleForm = async (data: FormProps) => { + if (createMembershipState.nextStep === "confirmPayment") { + } else { + createMembershipAction(data); + } + }; + const [step, setStep] = useState(1); - const validateStep1 = async (): Promise => { - await form.trigger(["firstName", "lastName", "email"]); - setStep(2); - }; - const validateStep2 = async (): Promise => { - await form.trigger(["socialSecurityNumber", "statuteApproval"]); - setStep(3); - }; + const validateStep1 = async (): Promise => { + const stepIsValid = await form.trigger(["firstName", "lastName", "email"]); + if (stepIsValid) setStep(2); + }; + const validateStep2 = async (): Promise => { + const stepIsValid = await form.trigger(["socialSecurityNumber", "statuteApproval"]); + if (stepIsValid) setStep(3); + }; - return ( -
-
-

- Activate a Membership -

-

- Fill the form below to request a membership number for the - association: Schroedinger Hat -

-
+ return ( +
+
+

+ Activate a Membership +

+

+ Fill the form below to request a membership number for the + association: Schroedinger Hat +

+
-
- - {/*Account data*/} - {createMembershipState.status === ServerActionStatus.Pending && - step === 1 && ( - <> -
-
- ( - - Name - - - - - - )} - /> -
-
- ( - - Surname - - - - - - )} - /> -
+ + + {/*Account data*/} + {createMembershipState.status === ServerActionStatus.Pending && + step === 1 && ( + <> +
+
+ ( + + Name + + + + + + )} + /> +
+
+ ( + + Surname + + + + + + )} + /> +
-
- ( - - Email - - - - - - )} - /> -
+
+ ( + + Email + + + + + + )} + /> +
-
-
- -
-
-
-

- By clicking Continue, you agree to our -
- - Terms of Service - {" "} - and{" "} - - Privacy Policy - - . -

- - )} +
+
+ +
+
+
- {/*Organisation required data*/} - {createMembershipState.status === ServerActionStatus.Pending && - step === 2 && ( -
-
- ( - - Social Security Number - - - - - - )} - /> -
+

+ By clicking Continue, you agree to our +
+ + Terms of Service + {" "} + and{" "} + + Privacy Policy + + . +

+ + )} -
-
- ( - -
- - - -
-

Statute

-

- I've read and approved{" "} - - the statute - -

- -
-
-
- )} - /> -
-
+ {/*Organisation required data*/} + {createMembershipState.status === ServerActionStatus.Pending && + step === 2 && ( +
+
+ ( + + Social Security Number + + + + + + )} + /> +
-
-
- -
-
-
- )} +
+
+ ( + +
+ + + +
+

Statute

+

+ I've read and approved{" "} + + the statute + +

+ +
+
+
+ )} + /> +
+
- {/*Membership level*/} - {createMembershipState.status === ServerActionStatus.Pending && - step === 3 && ( -
-
- -
+
+
+ +
+
+
+ )} -
-
- Subscribe -
-
-

- By clicking Subscribe, you will proceed to payment. -
- Payment is processed through our partner Stripe -

-
- )} + {/*Membership level*/} + {createMembershipState.status === ServerActionStatus.Pending && + step === 3 && ( +
+
+ +
- {/*Payment*/} - {createMembershipState.nextStep === "providePayment" && - step !== 5 && ( - - - - )} +
+
+ Subscribe +
+
+

+ By clicking Subscribe, you will proceed to payment. +
+ Payment is processed through our partner Stripe +

+
+ )} - {/*Success*/} - {step === 5 && ( -
- {"Success"} -

Welcome aboard!

-

- Admins will review your application and let know your membership - number in the following days. -

-
- )} -
- -
- ); + {/*Payment*/} + {createMembershipState.nextStep === "providePayment" && + step !== 5 && ( + + + + )} + + {/*Success*/} + {step === 5 && ( +
+ {"Success"} +

Welcome aboard!

+

+ Admins will review your application and let know your membership + number in the following days. +

+
+ )} + + +
+ ); } interface Step4Props { - state: ServerActionState; - setStep: Dispatch>; + state: ServerActionState; + setStep: Dispatch>; } -function Step4({ state, setStep }: Step4Props) { - const stripe = useStripe(); - const elements = useElements(); +function Step4({state, setStep}: Step4Props) { + const stripe = useStripe(); + const elements = useElements(); - const handlePaymentSubmit = async () => { - await elements?.submit(); - const confirmPayment = await stripe?.confirmPayment({ - clientSecret: (state?.payload as any)?.clientSecret, - elements: elements!, - redirect: "if_required", - }); - console.log(confirmPayment); - if ((confirmPayment?.paymentIntent as any)?.status === "succeeded") { - setStep(5); - } - }; + const handlePaymentSubmit = async () => { + await elements?.submit(); + const confirmPayment = await stripe?.confirmPayment({ + clientSecret: (state?.payload as any)?.clientSecret, + elements: elements!, + redirect: "if_required", + }); + console.log(confirmPayment); + if ((confirmPayment?.paymentIntent as any)?.status === "succeeded") { + setStep(5); + } + }; - return ( -
-

- Provide your payment informations to complete your membership - subscription -

+ return ( +
+

+ Provide your payment informations to complete your membership + subscription +

-
- -
+
+ +
-
-
- +
+
+ +
+
-
-
- ); + ); } diff --git a/src/components/devtool/debug.tsx b/src/components/devtool/debug.tsx index afdfaca..dc20abf 100644 --- a/src/components/devtool/debug.tsx +++ b/src/components/devtool/debug.tsx @@ -21,7 +21,7 @@ export function Debug({ title, children }: DebugInterface) { {title} )} -
+      
         {JSON.stringify(children, null, 4)}
       
diff --git a/src/app/signup/components/membershipCard.tsx b/src/components/molecules/membershipCard.tsx similarity index 100% rename from src/app/signup/components/membershipCard.tsx rename to src/components/molecules/membershipCard.tsx diff --git a/src/components/molecules/modal.tsx b/src/components/molecules/modal.tsx new file mode 100644 index 0000000..ee05550 --- /dev/null +++ b/src/components/molecules/modal.tsx @@ -0,0 +1,31 @@ +'use client' + +import {useRouter} from 'next/navigation' +import {Dialog, DialogContent, DialogDescription, DialogHeader} from "@/components/ui/dialog"; + +export function Modal({children}: { children: React.ReactNode }) { + const router = useRouter() + + return ( + <> + + + + + + + + {children} + + + + + + ) +} \ No newline at end of file diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index b5f1f7c..b0e3ce0 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -17,6 +17,7 @@ const buttonVariants = cva( icon: "h-9 w-9", lg: "h-10 rounded-md px-8", sm: "h-8 rounded-md px-3 text-xs", + xs: "h-6 rounded-md px-2 text-xs", }, variant: { default: diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..95b0d38 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { Cross2Icon } from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx new file mode 100644 index 0000000..c0df655 --- /dev/null +++ b/src/components/ui/table.tsx @@ -0,0 +1,120 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} From af1f54243a56659d57e192b586c23d21e59387da Mon Sep 17 00:00:00 2001 From: LobeTia Date: Sat, 13 Apr 2024 10:12:56 +0200 Subject: [PATCH 2/5] First working iteration --- package-lock.json | 190 +++++++++++++----- package.json | 7 +- .../migration.sql | 5 +- prisma/schema.prisma | 2 +- src/app/actions/createMembershipTemplate.ts | 40 ++++ src/app/actions/editMembershipTemplate.ts | 43 ++++ src/app/actions/types.ts | 23 ++- .../manage/@modal/(.)create/[id]/page.tsx | 9 - .../manage/@modal/(.)create/page.tsx | 11 + .../manage/@modal/(.)edit/[id]/page.tsx | 21 ++ .../manage/@modal/(..)create/[id]/page.tsx | 9 - .../manage/@modal/create/[id]/page.tsx | 7 - .../manage/@modal/create/client.tsx | 12 ++ .../membership/manage/@modal/create/page.tsx | 9 + .../manage/@modal/edit/[id]/client.tsx | 27 +++ .../manage/@modal/edit/[id]/page.tsx | 24 +++ .../membership/manage/@modal/page.tsx | 3 - .../@authenticated/membership/manage/form.tsx | 183 +++++++++++++++++ .../@authenticated/membership/manage/page.tsx | 30 +-- src/app/admin/layout.tsx | 2 + src/components/molecules/modal.tsx | 26 ++- src/components/ui/select.tsx | 164 +++++++++++++++ src/components/ui/textarea.tsx | 24 +++ src/modules/crudForm/types.tsx | 4 + 24 files changed, 764 insertions(+), 111 deletions(-) create mode 100644 src/app/actions/createMembershipTemplate.ts create mode 100644 src/app/actions/editMembershipTemplate.ts delete mode 100644 src/app/admin/@authenticated/membership/manage/@modal/(.)create/[id]/page.tsx create mode 100644 src/app/admin/@authenticated/membership/manage/@modal/(.)create/page.tsx create mode 100644 src/app/admin/@authenticated/membership/manage/@modal/(.)edit/[id]/page.tsx delete mode 100644 src/app/admin/@authenticated/membership/manage/@modal/(..)create/[id]/page.tsx delete mode 100644 src/app/admin/@authenticated/membership/manage/@modal/create/[id]/page.tsx create mode 100644 src/app/admin/@authenticated/membership/manage/@modal/create/client.tsx create mode 100644 src/app/admin/@authenticated/membership/manage/@modal/create/page.tsx create mode 100644 src/app/admin/@authenticated/membership/manage/@modal/edit/[id]/client.tsx create mode 100644 src/app/admin/@authenticated/membership/manage/@modal/edit/[id]/page.tsx delete mode 100644 src/app/admin/@authenticated/membership/manage/@modal/page.tsx create mode 100644 src/app/admin/@authenticated/membership/manage/form.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/textarea.tsx create mode 100644 src/modules/crudForm/types.tsx diff --git a/package-lock.json b/package-lock.json index ba239c0..46bf756 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,9 @@ "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-toast": "^1.1.5", "@react-email/components": "0.0.16", "@stripe/react-stripe-js": "^2.6.2", "@stripe/stripe-js": "^3.2.0", @@ -31,7 +33,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "lodash": "^4.17.21", - "next": "^14.1.3", + "next": "^14.2.0", "next-auth": "^4.24.6", "nodemailer": "^6.9.13", "react": "18.2.0", @@ -876,9 +878,9 @@ } }, "node_modules/@next/env": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.4.tgz", - "integrity": "sha512-e7X7bbn3Z6DWnDi75UWn+REgAbLEqxI8Tq2pkFOFAMpWAWApz/YCUhtWMWn410h8Q2fYiYL7Yg5OlxMOCfFjJQ==" + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.0.tgz", + "integrity": "sha512-4+70ELtSbRtYUuyRpAJmKC8NHBW2x1HMje9KO2Xd7IkoyucmV9SjgO+qeWMC0JWkRQXgydv1O7yKOK8nu/rITQ==" }, "node_modules/@next/eslint-plugin-next": { "version": "14.1.4", @@ -918,9 +920,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.4.tgz", - "integrity": "sha512-ubmUkbmW65nIAOmoxT1IROZdmmJMmdYvXIe8211send9ZYJu+SqxSnJM4TrPj9wmL6g9Atvj0S/2cFmMSS99jg==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.0.tgz", + "integrity": "sha512-kHktLlw0AceuDnkVljJ/4lTJagLzDiO3klR1Fzl2APDFZ8r+aTxNaNcPmpp0xLMkgRwwk6sggYeqq0Rz9K4zzA==", "cpu": [ "arm64" ], @@ -933,9 +935,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.4.tgz", - "integrity": "sha512-b0Xo1ELj3u7IkZWAKcJPJEhBop117U78l70nfoQGo4xUSvv0PJSTaV4U9xQBLvZlnjsYkc8RwQN1HoH/oQmLlQ==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.0.tgz", + "integrity": "sha512-HFSDu7lb1U3RDxXNeKH3NGRR5KyTPBSUTuIOr9jXoAso7i76gNYvnTjbuzGVWt2X5izpH908gmOYWtI7un+JrA==", "cpu": [ "x64" ], @@ -948,9 +950,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.4.tgz", - "integrity": "sha512-457G0hcLrdYA/u1O2XkRMsDKId5VKe3uKPvrKVOyuARa6nXrdhJOOYU9hkKKyQTMru1B8qEP78IAhf/1XnVqKA==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.0.tgz", + "integrity": "sha512-iQsoWziO5ZMxDWZ4ZTCAc7hbJ1C9UDj/gATSqTaMjW2bJFwAsvf9UM79AKnljBl73uPZ+V0kH4rvnHTco4Ps2w==", "cpu": [ "arm64" ], @@ -963,9 +965,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.4.tgz", - "integrity": "sha512-l/kMG+z6MB+fKA9KdtyprkTQ1ihlJcBh66cf0HvqGP+rXBbOXX0dpJatjZbHeunvEHoBBS69GYQG5ry78JMy3g==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.0.tgz", + "integrity": "sha512-0JOk2uzLUt8fJK5LpsKKZa74zAch7bJjjgJzR9aOMs231AlE4gPYzsSm430ckZitjPGKeH5bgDZjqwqJQKIS2w==", "cpu": [ "arm64" ], @@ -978,9 +980,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.4.tgz", - "integrity": "sha512-BapIFZ3ZRnvQ1uWbmqEGJuPT9cgLwvKtxhK/L2t4QYO7l+/DxXuIGjvp1x8rvfa/x1FFSsipERZK70pewbtJtw==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.0.tgz", + "integrity": "sha512-uYHkuTzX0NM6biKNp7hdKTf+BF0iMV254SxO0B8PgrQkxUBKGmk5ysHKB+FYBfdf9xei/t8OIKlXJs9ckD943A==", "cpu": [ "x64" ], @@ -993,9 +995,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.4.tgz", - "integrity": "sha512-mqVxTwk4XuBl49qn2A5UmzFImoL1iLm0KQQwtdRJRKl21ylQwwGCxJtIYo2rbfkZHoSKlh/YgztY0qH3wG1xIg==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.0.tgz", + "integrity": "sha512-paN89nLs2dTBDtfXWty1/NVPit+q6ldwdktixYSVwiiAz647QDCd+EIYqoiS+/rPG3oXs/A7rWcJK9HVqfnMVg==", "cpu": [ "x64" ], @@ -1008,9 +1010,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.4.tgz", - "integrity": "sha512-xzxF4ErcumXjO2Pvg/wVGrtr9QQJLk3IyQX1ddAC/fi6/5jZCZ9xpuL9Tzc4KPWMFq8GGWFVDMshZOdHGdkvag==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.0.tgz", + "integrity": "sha512-j1oiidZisnymYjawFqEfeGNcE22ZQ7lGUaa4pGOCVWrWeIDkPSj8zYgS9TzMNlg17Q3wSWCQC/F5uJAhSh7qcA==", "cpu": [ "arm64" ], @@ -1023,9 +1025,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.4.tgz", - "integrity": "sha512-WZiz8OdbkpRw6/IU/lredZWKKZopUMhcI2F+XiMAcPja0uZYdMTZQRoQ0WZcvinn9xZAidimE7tN9W5v9Yyfyw==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.0.tgz", + "integrity": "sha512-6ff6F4xb+QGD1jhx/dOT9Ot7PQ/GAYekV9ykwEh2EFS/cLTyU4Y3cXkX5cNtNIhpctS5NvyjW9gIksRNErYE0A==", "cpu": [ "ia32" ], @@ -1038,9 +1040,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.4.tgz", - "integrity": "sha512-4Rto21sPfw555sZ/XNLqfxDUNeLhNYGO2dlPqsnuCg8N8a2a9u1ltqBOPQ4vj1Gf7eJC0W2hHG2eYUHuiXgY2w==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.0.tgz", + "integrity": "sha512-09DbG5vXAxz0eTFSf1uebWD36GF3D5toynRkgo2AlSrxwGZkWtJ1RhmrczRYQ17eD5bdo4FZ0ibiffdq5kc4vg==", "cpu": [ "x64" ], @@ -1185,6 +1187,14 @@ "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-1.0.1.tgz", "integrity": "sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg==" }, + "node_modules/@radix-ui/number": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", + "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", @@ -1746,6 +1756,49 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz", + "integrity": "sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", @@ -1764,6 +1817,40 @@ } } }, + "node_modules/@radix-ui/react-toast": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz", + "integrity": "sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toggle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz", @@ -7727,12 +7814,12 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "node_modules/next": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/next/-/next-14.1.4.tgz", - "integrity": "sha512-1WTaXeSrUwlz/XcnhGTY7+8eiaFvdet5z9u3V2jb+Ek1vFo0VhHKSAIJvDWfQpttWjnyw14kBeq28TPq7bTeEQ==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.0.tgz", + "integrity": "sha512-2T41HqJdKPqheR27ll7MFZ3gtTYvGew7cUc0PwPSyK9Ao5vvwpf9bYfP4V5YBGLckHF2kEGvrLte5BqLSv0s8g==", "dependencies": { - "@next/env": "14.1.4", - "@swc/helpers": "0.5.2", + "@next/env": "14.2.0", + "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", @@ -7746,18 +7833,19 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.1.4", - "@next/swc-darwin-x64": "14.1.4", - "@next/swc-linux-arm64-gnu": "14.1.4", - "@next/swc-linux-arm64-musl": "14.1.4", - "@next/swc-linux-x64-gnu": "14.1.4", - "@next/swc-linux-x64-musl": "14.1.4", - "@next/swc-win32-arm64-msvc": "14.1.4", - "@next/swc-win32-ia32-msvc": "14.1.4", - "@next/swc-win32-x64-msvc": "14.1.4" + "@next/swc-darwin-arm64": "14.2.0", + "@next/swc-darwin-x64": "14.2.0", + "@next/swc-linux-arm64-gnu": "14.2.0", + "@next/swc-linux-arm64-musl": "14.2.0", + "@next/swc-linux-x64-gnu": "14.2.0", + "@next/swc-linux-x64-musl": "14.2.0", + "@next/swc-win32-arm64-msvc": "14.2.0", + "@next/swc-win32-ia32-msvc": "14.2.0", + "@next/swc-win32-x64-msvc": "14.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" @@ -7766,6 +7854,9 @@ "@opentelemetry/api": { "optional": true }, + "@playwright/test": { + "optional": true + }, "sass": { "optional": true } @@ -7814,6 +7905,15 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/next/node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/package.json b/package.json index 3781c41..646d7e1 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "postinstall": "prisma generate", "lint": "next lint", "lint:fix": "next lint --fix", - "start": "next start" + "start": "next start", + "ragequit": "rm -rf .next && npm run db:push && npm run dev" }, "dependencies": { "@auth/prisma-adapter": "^1.4.0", @@ -29,7 +30,9 @@ "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-toast": "^1.1.5", "@react-email/components": "0.0.16", "@stripe/react-stripe-js": "^2.6.2", "@stripe/stripe-js": "^3.2.0", @@ -39,7 +42,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "lodash": "^4.17.21", - "next": "^14.1.3", + "next": "^14.2.0", "next-auth": "^4.24.6", "nodemailer": "^6.9.13", "react": "18.2.0", diff --git a/prisma/migrations/20240411161725_add_membership_template/migration.sql b/prisma/migrations/20240411161725_add_membership_template/migration.sql index 8daa0e4..b58c046 100644 --- a/prisma/migrations/20240411161725_add_membership_template/migration.sql +++ b/prisma/migrations/20240411161725_add_membership_template/migration.sql @@ -14,9 +14,10 @@ CREATE TABLE "MembershipTemplate" ( "pricePeriod" "PricePeriod" NOT NULL DEFAULT 'Yearly', "priceUnit" "PriceUnit" NOT NULL DEFAULT 'EUR', "stripePriceId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL; CONSTRAINT "MembershipTemplate_pkey" PRIMARY KEY ("id") ); -ALTER TABLE "MembershipTemplate" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, -ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; \ No newline at end of file +CREATE UNIQUE INDEX "MembershipTemplate_stripePriceId_key" ON "MembershipTemplate"("stripePriceId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8bf99ff..2987f04 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -116,7 +116,7 @@ model MembershipTemplate { priceAmount Int pricePeriod PricePeriod @default(Yearly) priceUnit PriceUnit @default(EUR) - stripePriceId String + stripePriceId String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } diff --git a/src/app/actions/createMembershipTemplate.ts b/src/app/actions/createMembershipTemplate.ts new file mode 100644 index 0000000..467f117 --- /dev/null +++ b/src/app/actions/createMembershipTemplate.ts @@ -0,0 +1,40 @@ +"use server"; + +import {type ServerActionState, ServerActionStatus,} from "@/app/actions/types"; +import {db} from "@/services/db"; +import {PricePeriod, PriceUnit} from "@prisma/client"; + +export interface FormProps { + title: string + description?: string + features?: string + priceAmount: number + priceUnit: PriceUnit + stripePriceId: string +} + +export async function createMembershipTemplate( + prevState: ServerActionState, + data: FormProps, +): Promise { + // Query to create user + const membershipTemplate = await db.membershipTemplate.create({ + data: { + title: data.title, + description: data.description, + features: (data.features && data.features.length) ? data.features.split(',').filter(Boolean) : [], + priceAmount: data.priceAmount * 100, + priceUnit: data.priceUnit, + pricePeriod: PricePeriod.Yearly, + stripePriceId: data.stripePriceId, + createdAt: new Date(), + updatedAt: new Date(), + } + }); + + return { + status: ServerActionStatus.Success, + payload: membershipTemplate + } + +} diff --git a/src/app/actions/editMembershipTemplate.ts b/src/app/actions/editMembershipTemplate.ts new file mode 100644 index 0000000..bb29132 --- /dev/null +++ b/src/app/actions/editMembershipTemplate.ts @@ -0,0 +1,43 @@ +"use server"; + +import {type ServerActionState, ServerActionStatus,} from "@/app/actions/types"; +import {db} from "@/services/db"; +import {PricePeriod, PriceUnit} from "@prisma/client"; + +export interface FormProps { + id: string + title: string + description?: string + features?: string + priceAmount: number + priceUnit: PriceUnit + stripePriceId: string +} + +export async function editMembershipTemplate( + prevState: ServerActionState, + data: FormProps, +): Promise { + // Query to create user + const membershipTemplate = await db.membershipTemplate.update({ + where:{ + id: data.id + }, + data: { + title: data.title, + description: data.description, + features: (data.features && data.features.length) ? data.features.split(',').filter(Boolean) : [], + priceAmount: data.priceAmount * 100, + priceUnit: data.priceUnit, + pricePeriod: PricePeriod.Yearly, + stripePriceId: data.stripePriceId, + updatedAt: new Date(), + } + }); + + return { + status: ServerActionStatus.Success, + payload: membershipTemplate + } + +} diff --git a/src/app/actions/types.ts b/src/app/actions/types.ts index ff699af..30c3708 100644 --- a/src/app/actions/types.ts +++ b/src/app/actions/types.ts @@ -1,17 +1,22 @@ export interface ErrorItem { - code?: number; - message: string; + code?: number; + message: string; } export interface ServerActionState { - status: ServerActionStatus; - payload?: object; - nextStep?: string; - errors?: ErrorItem[]; + status: ServerActionStatus; + payload?: object; + nextStep?: string; + errors?: ErrorItem[]; } export enum ServerActionStatus { - Success = "success", - Error = "error", - Pending = "pending", + Success = "success", + Error = "error", + Pending = "pending", } + +export const InitialServerActionState = { + payload: {}, + status: ServerActionStatus.Pending, +} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/manage/@modal/(.)create/[id]/page.tsx b/src/app/admin/@authenticated/membership/manage/@modal/(.)create/[id]/page.tsx deleted file mode 100644 index 75cc3de..0000000 --- a/src/app/admin/@authenticated/membership/manage/@modal/(.)create/[id]/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import {Modal} from "@/components/molecules/modal"; - -export default function AdminMembershipModalCreatePage({ - params: {id}, - }: { - params: { id: string }; -}) { - return AdminMembershipModalCreatePage: {id} -} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/manage/@modal/(.)create/page.tsx b/src/app/admin/@authenticated/membership/manage/@modal/(.)create/page.tsx new file mode 100644 index 0000000..8f219fb --- /dev/null +++ b/src/app/admin/@authenticated/membership/manage/@modal/(.)create/page.tsx @@ -0,0 +1,11 @@ +import {Modal} from "@/components/molecules/modal"; +import { + AdminMembershipCRUDForm +} from "@/app/admin/@authenticated/membership/manage/form"; +import {CRUDFormIntent} from "@/modules/crudForm/types"; + +export default function AdminMembershipModalCreatePage() { + return + + +} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/manage/@modal/(.)edit/[id]/page.tsx b/src/app/admin/@authenticated/membership/manage/@modal/(.)edit/[id]/page.tsx new file mode 100644 index 0000000..b0c49e0 --- /dev/null +++ b/src/app/admin/@authenticated/membership/manage/@modal/(.)edit/[id]/page.tsx @@ -0,0 +1,21 @@ +import {Modal} from "@/components/molecules/modal"; +import { + AdminMembershipCRUDForm +} from "@/app/admin/@authenticated/membership/manage/form"; +import {CRUDFormIntent} from "@/modules/crudForm/types"; +import {db} from "@/services/db"; + +const getData = (id: string) => { + return db.membershipTemplate.findUnique({ + where: { + id + } + }) +} +export default async function AdminMembershipModalEditPage({params}) { + const membershipTemplate = await getData(params.id) + + return + + +} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/manage/@modal/(..)create/[id]/page.tsx b/src/app/admin/@authenticated/membership/manage/@modal/(..)create/[id]/page.tsx deleted file mode 100644 index 75cc3de..0000000 --- a/src/app/admin/@authenticated/membership/manage/@modal/(..)create/[id]/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import {Modal} from "@/components/molecules/modal"; - -export default function AdminMembershipModalCreatePage({ - params: {id}, - }: { - params: { id: string }; -}) { - return AdminMembershipModalCreatePage: {id} -} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/manage/@modal/create/[id]/page.tsx b/src/app/admin/@authenticated/membership/manage/@modal/create/[id]/page.tsx deleted file mode 100644 index 38a0359..0000000 --- a/src/app/admin/@authenticated/membership/manage/@modal/create/[id]/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function AdminMembershipCreatePage({ - params: { id }, - }: { - params: { id: string }; -}) { - return
AdminMembershipCreatePage: {id}
; -} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/manage/@modal/create/client.tsx b/src/app/admin/@authenticated/membership/manage/@modal/create/client.tsx new file mode 100644 index 0000000..e38593f --- /dev/null +++ b/src/app/admin/@authenticated/membership/manage/@modal/create/client.tsx @@ -0,0 +1,12 @@ +'use client' + +import {AdminMembershipCRUDForm} from "@/app/admin/@authenticated/membership/manage/form"; +import {CRUDFormIntent} from "@/modules/crudForm/types"; + +export function AdminMembershipCreatePageClient() { + const onSubmit = () => { + alert('Done') + } + + return +} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/manage/@modal/create/page.tsx b/src/app/admin/@authenticated/membership/manage/@modal/create/page.tsx new file mode 100644 index 0000000..764c264 --- /dev/null +++ b/src/app/admin/@authenticated/membership/manage/@modal/create/page.tsx @@ -0,0 +1,9 @@ +import {CRUDFormIntent} from "@/modules/crudForm/types"; +import {AdminMembershipCreatePageClient} from "@/app/admin/@authenticated/membership/manage/@modal/create/client"; + +export default function AdminMembershipCreatePage() { + return
+ +
; +} + diff --git a/src/app/admin/@authenticated/membership/manage/@modal/edit/[id]/client.tsx b/src/app/admin/@authenticated/membership/manage/@modal/edit/[id]/client.tsx new file mode 100644 index 0000000..cf60fca --- /dev/null +++ b/src/app/admin/@authenticated/membership/manage/@modal/edit/[id]/client.tsx @@ -0,0 +1,27 @@ +'use client' + +import {AdminMembershipCRUDForm} from "@/app/admin/@authenticated/membership/manage/form"; +import {CRUDFormIntent} from "@/modules/crudForm/types"; +import {MembershipTemplate} from "@prisma/client"; +import {useToast} from "@/components/ui/use-toast"; +import {useRouter} from "next/navigation"; + +interface AdminMembershipEditPageClient { + previousValues: MembershipTemplate +} + +export function AdminMembershipEditPageClient({previousValues}: AdminMembershipEditPageClient) { + const {toast}=useToast() + const router = useRouter() + + const onSubmit = () => { + toast({ + title:'Success', + description:'Membership Template updated' + }) + router.replace('/admin/membership/manage') + } + + return +} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/manage/@modal/edit/[id]/page.tsx b/src/app/admin/@authenticated/membership/manage/@modal/edit/[id]/page.tsx new file mode 100644 index 0000000..1c93762 --- /dev/null +++ b/src/app/admin/@authenticated/membership/manage/@modal/edit/[id]/page.tsx @@ -0,0 +1,24 @@ +import {db} from "@/services/db"; +import {AdminMembershipEditPageClient} from "@/app/admin/@authenticated/membership/manage/@modal/edit/[id]/client"; + +const getData = (id: string) => { + return db.membershipTemplate.findUnique({ + where: { + id + } + }) +} + +interface AdminMembershipEditPageProps { + params: { + id: string + } +} + +export default async function AdminMembershipEditPage({params}: AdminMembershipEditPageProps) { + const membershipTemplate = await getData(params.id) + + return
+ +
; +} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/manage/@modal/page.tsx b/src/app/admin/@authenticated/membership/manage/@modal/page.tsx deleted file mode 100644 index 4f6b2c5..0000000 --- a/src/app/admin/@authenticated/membership/manage/@modal/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function AdminMembershipModalPage(){ - return
PAGEEEE
-} \ No newline at end of file diff --git a/src/app/admin/@authenticated/membership/manage/form.tsx b/src/app/admin/@authenticated/membership/manage/form.tsx new file mode 100644 index 0000000..450b1b8 --- /dev/null +++ b/src/app/admin/@authenticated/membership/manage/form.tsx @@ -0,0 +1,183 @@ +'use client' + +import {useForm} from "react-hook-form"; +import {z} from "zod"; +import {zodResolver} from "@hookform/resolvers/zod"; +import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form"; +import {Input} from "@/components/ui/input"; +import {Textarea} from "@/components/ui/textarea"; +import {StatefulButton} from "@/components/molecules/statefulButton"; +import {useFormState} from "react-dom"; +import {InitialServerActionState, ServerActionStatus} from "@/app/actions/types"; +import {createMembershipTemplate} from "@/app/actions/createMembershipTemplate"; +import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select"; +import {CRUDFormIntent} from "@/modules/crudForm/types"; +import {editMembershipTemplate} from "@/app/actions/editMembershipTemplate"; +import {useEffect} from "react"; + +const formSchema = z.object({ + id: z.string() + .optional(), + title: z.string() + .min(4, 'Title should be at least 4 characters long'), + description: z.string() + .optional(), + features: z.string() + .optional(), + priceAmount: z.coerce.number() + .positive(), + priceUnit: z.string(), + stripePriceId: z.string() + .regex(new RegExp('^price_[0-9A-Za-z]+$'), 'You need to provide a Stripe Product Price ID, format: price_xxxxx'), +}); + +interface FormProps { + intent: CRUDFormIntent + previousValues?: any + onSuccess?: any +} + +export function AdminMembershipCRUDForm({intent, previousValues, onSuccess}: FormProps) { + const [membershipTemplateState, membershipTemplateAction] = useFormState( + (intent === CRUDFormIntent.Create) ? createMembershipTemplate : editMembershipTemplate, + InitialServerActionState + ); + const form = useForm>({ + defaultValues: (intent === CRUDFormIntent.Create) + ? {} + : { + ...previousValues, + features: previousValues?.features.join(','), + priceAmount: previousValues.priceAmount / 100 + }, + resolver: zodResolver(formSchema), + }); + + useEffect(() => { + if (membershipTemplateState.status === ServerActionStatus.Success && onSuccess) { + onSuccess(membershipTemplateState) + } + }, [membershipTemplateState, onSuccess]) + + return
+ + { + CRUDFormIntent.Edit && ( + + )} + /> + } +
+ ( + + Title + + + + + + )} + /> + + ( + + Description + +