Skip to content

Commit

Permalink
Merge pull request #6 from timia2109/feature/create-invitations
Browse files Browse the repository at this point in the history
Feature/create invitations
  • Loading branch information
timia2109 authored Aug 4, 2024
2 parents 8352a34 + ea1b65b commit 6592511
Show file tree
Hide file tree
Showing 26 changed files with 619 additions and 90 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
"dependencies": {
"@auth/prisma-adapter": "^2.4.1",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-brands-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@prisma/client": "^4.16.2",
"@prisma/client": "^5.17.0",
"@types/luxon": "^3.4.2",
"@types/randomstring": "^1.3.0",
"classnames": "^2.5.1",
Expand Down
36 changes: 20 additions & 16 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `expiresAt` to the `MealPlanInvite` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE `MealPlanInvite` ADD COLUMN `expiresAt` DATETIME(3) NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
Warnings:
- Made the column `createdByUserId` on table `MealPlanInvite` required. This step will fail if there are existing NULL values in that column.
*/
-- DropForeignKey
ALTER TABLE `MealPlanInvite` DROP FOREIGN KEY `MealPlanInvite_createdByUserId_fkey`;

-- AlterTable
ALTER TABLE `MealPlanInvite` MODIFY `createdByUserId` VARCHAR(191) NOT NULL;

-- AddForeignKey
ALTER TABLE `MealPlanInvite` ADD CONSTRAINT `MealPlanInvite_createdByUserId_fkey` FOREIGN KEY (`createdByUserId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `MealPlanInvite` ADD CONSTRAINT `MealPlanInvite_mealPlanId_fkey` FOREIGN KEY (`mealPlanId`) REFERENCES `MealPlan`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
7 changes: 5 additions & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ model MealPlan {
title String @db.Text
mealPlanAssignments MealPlanAssignment[]
mealEntries MealEntry[]
invitations MealPlanInvite[]
}

model MealPlanAssignment {
Expand All @@ -43,9 +44,11 @@ model MealPlanInvite {
invitationCode String @id @db.Char(12)
mealPlanId String @db.Char(25)
createdAt DateTime @default(now())
createdByUserId String?
createdByUserId String
expiresAt DateTime
user User? @relation(fields: [createdByUserId], references: [id], onDelete: SetNull)
user User @relation(fields: [createdByUserId], references: [id], onDelete: Cascade)
mealPlan MealPlan @relation(fields: [mealPlanId], references: [id], onDelete: Cascade)
}

model User {
Expand Down
18 changes: 18 additions & 0 deletions src/actions/acceptInvitationAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use server";

import { getInvitation } from "@/dal/user/getInvitation";
import { redeemMealPlanInvitation } from "@/dal/user/redeemMealPlanInvitation";
import { getUserId } from "@/functions/user/getUserId";
import { redirectWithLocale } from "@/functions/user/redirectWithLocale";

export async function acceptInvitationAction(invitationCode: string) {
const userId = await getUserId(null);
const invitation = await getInvitation(invitationCode, userId);

if (invitation.result != "OK") {
throw new Error("Invitation not found");
}

await redeemMealPlanInvitation(invitation.invitation, userId);
redirectWithLocale(`/mealPlan/${invitation.invitation.mealPlan.id}`);
}
47 changes: 45 additions & 2 deletions src/app/[locale]/(landing)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,42 @@
import { auth } from "@/auth";
import { InvitationHeader } from "@/components/invitation/InvitationHeader";
import { getInvitation } from "@/dal/user/getInvitation";
import { redirectWithLocale } from "@/functions/user/redirectWithLocale";
import { getScopedI18n } from "@/locales/server";
import { SignInButtons } from "./SignInButtons";

export default async function LandingPage() {
type Props = {
searchParams: {
invitationCode?: string;
};
};

async function handleInvitation(
invitationCode: string | undefined,
isSignedIn: boolean
) {
if (invitationCode === undefined) return undefined;

const invitation = await getInvitation(invitationCode);

if (isSignedIn) {
// Redirect to join page
redirectWithLocale(`/mealPlan/join/${invitationCode}`);
}

return invitation;
}

export default async function LandingPage({ searchParams }: Props) {
const t = await getScopedI18n("landing");

const currentUser = await auth();
if (currentUser != null) redirectWithLocale(`/mealPlan`);
const invitation = await handleInvitation(
searchParams.invitationCode,
currentUser != null
);
if (currentUser != null && invitation == null)
redirectWithLocale(`/mealPlan`);

return (
<div className="p-12">
Expand All @@ -23,6 +52,11 @@ export default async function LandingPage() {

<p className="text-center text-xl md:w-1/2">{t("subtitle")}</p>
</div>

{invitation !== undefined && (
<InvitationHeader invitation={invitation} />
)}

<SignInButtons />
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
<div className="card w-96 bg-base-300 shadow-2xl">
Expand All @@ -33,6 +67,15 @@ export default async function LandingPage() {
<p>Feature Box</p>
</div>
</div>

<div className="card w-96 bg-base-300 shadow-2xl">
<div className="card-body">
<h2 className="card-title flex justify-center">Search Params</h2>
<p>
<code>{JSON.stringify(searchParams)}</code>
</p>
</div>
</div>
</div>
</div>
</div>
Expand Down
50 changes: 48 additions & 2 deletions src/app/[locale]/(userArea)/mealPlan/invite/[mealPlanId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,49 @@
export default function InvitePage() {
return <div>InvitePage</div>;
import { Heading } from "@/components/common/Heading";
import { SocialShareLinks } from "@/components/common/SocialShareLinks";
import { createMealPlanInvitation } from "@/dal/mealPlans/createMealPlanInvitation";
import { getMealPlan } from "@/dal/mealPlans/getMealPlan";
import { buildUrl } from "@/functions/buildUrl";
import { getUserId } from "@/functions/user/getUserId";
import { getScopedI18n } from "@/locales/server";
import { notFound } from "next/navigation";

type Props = {
params: {
mealPlanId: string;
};
};

export default async function InvitePage({ params }: Props) {
const userId = await getUserId(true);
const mealPlan = await getMealPlan(userId, params.mealPlanId);
if (mealPlan == null) notFound();

const t = await getScopedI18n("invite");

const invitation = await createMealPlanInvitation(params.mealPlanId, userId);

const invitationLink = buildUrl({
path: "/",
search: {
invitationCode: invitation.invitationCode,
},
});

return (
<div className="container mx-auto">
<Heading>{t("invite", mealPlan)}</Heading>
<p>{t("inviteMessage")}</p>
<p>{t("inviteHint")}</p>
<div className="cursor-grab select-all rounded-sm bg-indigo-50 px-1 text-lg text-indigo-950">
<code>{invitationLink.toString()}</code>
</div>
<div className="mt-3">
<SocialShareLinks
messagePayload={t("shareText", {
invitationLink: invitationLink.toString(),
})}
/>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use client";

import { acceptInvitationAction } from "@/actions/acceptInvitationAction";
import type { FC, PropsWithChildren } from "react";

type Props = {
invitationCode: string;
};

export const AcceptButton: FC<PropsWithChildren<Props>> = ({
invitationCode,
children,
}) => (
<button
className="btn btn-primary"
onClick={() => acceptInvitationAction(invitationCode)}
>
{children}
</button>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ProfileImage } from "@/components/common/ProfileImage";
import { InvitationHeader } from "@/components/invitation/InvitationHeader";
import { getInvitation } from "@/dal/user/getInvitation";
import { getMealPlanLabel } from "@/functions/user/getMealPlanLabel";
import { getUserId } from "@/functions/user/getUserId";
import { redirectWithLocale } from "@/functions/user/redirectWithLocale";
import { getI18n, getScopedI18n } from "@/locales/server";
import { AcceptButton } from "./AcceptButton";

type Props = {
params: {
invitationCode: string;
};
};

export default async function InvitationPage({ params }: Props) {
const userId = await getUserId(true);
const invitation = await getInvitation(params.invitationCode, userId);

// Redirect if joined
if (invitation.result === "JOINED") {
redirectWithLocale(`/mealPlan/${invitation.invitation.mealPlan.id}`);
}

const t = await getScopedI18n("invitation");
const mealPlanTitle =
invitation.result === "OK"
? await getMealPlanLabel(invitation.invitation.mealPlan, await getI18n())
: "";

return (
<div className="container mx-auto flex justify-center align-middle">
<InvitationHeader invitation={invitation} hideOnSuccess />
{invitation.result === "OK" && (
<div className="card w-96 bg-base-100 shadow-xl">
<div className="flex justify-center pt-1">
<ProfileImage user={invitation.invitation.user} />
</div>
<div className="card-body items-center text-center">
<h2 className="card-title">{t("header")}</h2>
<p>
{t("loginToJoinTitle", {
mealPlanTitle,
name: invitation.invitation.user.name ?? t("unknownUser"),
})}
</p>
<div className="card-actions">
<AcceptButton
invitationCode={invitation.invitation.invitationCode}
>
{t("accept", {
mealPlanTitle,
})}
</AcceptButton>
</div>
</div>
</div>
)}
</div>
);
}
Loading

0 comments on commit 6592511

Please sign in to comment.