Skip to content

Commit

Permalink
feat: intergation notification with backend & calendar tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
qamarq committed Dec 29, 2024
1 parent c26eaf3 commit e199edf
Show file tree
Hide file tree
Showing 13 changed files with 224 additions and 20 deletions.
Binary file added frontend/public/assets/tutorial/tutorial-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/tutorial/tutorial-2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/tutorial/tutorial-3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/tutorial/tutorial-4.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions frontend/src/actions/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use server";

import { auth, fetchToAdonis } from "@/lib/auth";
import type { UserSettingsPayload } from "@/types";

export const getCurrentUser = async () => {
const user = await auth();
if (user == null) {
throw new Error("Not logged in");
}
return user;
};

interface UpdateUserSettingsResponse {
message: string;
user: number;
success: boolean;
}

export const updateUser = async (payload: UserSettingsPayload) => {
const user = await auth();
if (user == null) {
throw new Error("Not logged in");
}

// Update user
const data = await fetchToAdonis<UpdateUserSettingsResponse>({
url: "/user",
method: "PATCH",
body: JSON.stringify(payload),
});

if (data === null) {
throw new Error("Failed to update user");
}

return { success: true };
};
58 changes: 47 additions & 11 deletions frontend/src/app/plans/_components/notifications-form.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Loader2Icon } from "lucide-react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";

import { getCurrentUser, updateUser } from "@/actions/user";
import { Button } from "@/components/ui/button";
import {
Form,
Expand All @@ -15,30 +18,60 @@ import {
FormLabel,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import type { User, UserSettingsPayload } from "@/types";

const notificationsFormSchema = z.object({
plan_changes_emails: z.boolean(),
allow_notifications: z.boolean(),
security_emails: z.boolean(),
});

type NotificationsFormValues = z.infer<typeof notificationsFormSchema>;

export function NotificationsForm() {
export function NotificationsForm({ defaultUser }: { defaultUser: User }) {
const { data: user, refetch } = useQuery({
queryKey: ["user", defaultUser.id],
queryFn: async () => {
const response = await getCurrentUser();
return response;
},
initialData: defaultUser,
});

const { mutateAsync: updateUserFunction, isPending: isUpdating } =
useMutation({
mutationKey: ["updateUser"],
mutationFn: async (payload: UserSettingsPayload) => {
await updateUser(payload);
},
});
const form = useForm<NotificationsFormValues>({
resolver: zodResolver(notificationsFormSchema),
defaultValues: {
plan_changes_emails: true,
allow_notifications: user.allowNotifications,
security_emails: true,
},
});

function onSubmit(data: NotificationsFormValues) {
toast("You submitted the following values:", {
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
),
// eslint-disable-next-line no-async-promise-executor
const updateUserPromise = new Promise(async (resolve, reject) => {
try {
await updateUserFunction({
allowNotifications: data.allow_notifications,
});
await refetch();
resolve({ success: true });
} catch {
reject(new Error("Failed to update user settings"));
}
});

toast.promise(updateUserPromise, {
loading: "Aktualizowanie...",
success: () => {
return `Zaktualizowano pomyślnie!`;
},
error: "Wystąpił błąd podczas aktualizacji.",
});
}

Expand All @@ -50,7 +83,7 @@ export function NotificationsForm() {
<div className="space-y-4">
<FormField
control={form.control}
name="plan_changes_emails"
name="allow_notifications"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
Expand Down Expand Up @@ -97,7 +130,10 @@ export function NotificationsForm() {
</div>
</div>

<Button type="submit">Zaktualizuj dane</Button>
<Button type="submit" disabled={isUpdating}>
{isUpdating ? <Loader2Icon className="size-4 animate-spin" /> : null}
Zaktualizuj dane
</Button>
</form>
</Form>
);
Expand Down
85 changes: 85 additions & 0 deletions frontend/src/app/plans/account/calendar/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import Image from "next/image";
import React from "react";

import { Separator } from "@/components/ui/separator";

import TutorialImage1 from "/public/assets/tutorial/tutorial-1.png";
import TutorialImage2 from "/public/assets/tutorial/tutorial-2.jpg";
import TutorialImage3 from "/public/assets/tutorial/tutorial-3.jpg";
import TutorialImage4 from "/public/assets/tutorial/tutorial-4.jpg";

export default function FAQCalendarPage() {
return (
<div className="w-full space-y-6">
<div>
<h3 className="text-lg font-medium">Jak dodać do kalendarza</h3>
<p className="text-sm text-muted-foreground">
Tutaj znajdziesz krótki tutorial, jak dodać swój plan zajęć do swojego
kalendarza Google
</p>
</div>
<Separator />
<div className="space-y-3">
<Title title={"Pobierz plik .ics"} step={1} />
<p className="">
Kliknij przycisk &quot;Dodaj do kalendarza (.ics)&quot;
</p>
<Image
src={TutorialImage1}
alt="Tutorial 1"
unoptimized
className="w-full"
/>
</div>
<div className="space-y-3">
<Title
title={"Przejdź do kalendarza google i kliknij importuj"}
step={2}
/>
<p className="">
Przejdź na stronę kalendarza Google i kliknij plus w dolnym lewym rogu
&gt; importuj
</p>
<Image
src={TutorialImage2}
alt="Tutorial 1"
unoptimized
className="w-full"
/>
<Image
src={TutorialImage3}
alt="Tutorial 1"
unoptimized
className="w-full"
/>
</div>
<div className="space-y-3">
<Title title={"Wybierz pobrany plik"} step={3} />
<p className="">
Wybierz plik .ics, który pobrałeś wcześniej z naszej strony i kliknij
importuj
</p>
<Image
src={TutorialImage4}
alt="Tutorial 1"
unoptimized
className="w-full"
/>
</div>
<div className="space-y-3">
<Title title={"Ciesz się zaimportowanymi wydarzeniami"} step={4} />
</div>
</div>
);
}

function Title({ title, step }: { title: string; step: number }) {
return (
<div className="flex items-center gap-3">
<div className="flex size-10 items-center justify-center rounded-md bg-primary text-lg font-semibold text-white">
{step}.
</div>
<h1 className="text-lg font-semibold">{title}</h1>
</div>
);
}
13 changes: 9 additions & 4 deletions frontend/src/app/plans/account/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BellRingIcon, UserIcon } from "lucide-react";
import { BellRingIcon, CalendarPlusIcon, UserIcon } from "lucide-react";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import React from "react";
Expand All @@ -24,6 +24,11 @@ const sidebarNavItems = [
href: "/plans/account/notifications",
icon: <BellRingIcon className="size-4" />,
},
{
title: "Dodawanie do kalendarza",
href: "/plans/account/calendar",
icon: <CalendarPlusIcon className="size-4" />,
},
];

export default async function SettingsLayout({
Expand All @@ -37,8 +42,8 @@ export default async function SettingsLayout({
}

return (
<div className="h-screen w-full">
<div className="container mx-auto hidden space-y-6 p-10 pb-16 md:block">
<div className="w-full pb-10">
<div className="container mx-auto flex flex-col space-y-6 p-10 pb-0">
<div className="space-y-0.5">
<h2 className="text-2xl font-bold tracking-tight">Ustawienia</h2>
<p className="text-muted-foreground">
Expand All @@ -50,7 +55,7 @@ export default async function SettingsLayout({
<aside className="-mx-4 lg:w-1/5">
<SidebarSettings items={sidebarNavItems} />
</aside>
<div className="flex-1 lg:max-w-2xl">{children}</div>
<div className="flex-1 lg:max-w-3xl">{children}</div>
</div>
</div>
</div>
Expand Down
16 changes: 14 additions & 2 deletions frontend/src/app/plans/account/notifications/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import { notFound } from "next/navigation";
import React from "react";

import { Separator } from "@/components/ui/separator";
import { auth } from "@/lib/auth";

import { NotificationsForm } from "../../_components/notifications-form";

export default function NotificationsPage() {
export default async function NotificationsPage() {
let user;
try {
user = await auth();
if (user == null) {
throw new Error("Not logged in");
}
} catch {
return notFound();
}

return (
<div className="space-y-6">
<div>
Expand All @@ -14,7 +26,7 @@ export default function NotificationsPage() {
</p>
</div>
<Separator />
<NotificationsForm />
<NotificationsForm defaultUser={user} />
</div>
);
}
9 changes: 8 additions & 1 deletion frontend/src/app/plans/account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ const USER_STATUSES = {
2: "Aktywny Student",
};

const USER_SEX = {
M: "Mężczyzna",
K: "Kobieta",
};

export default async function ProfilePage() {
let profile;

Expand All @@ -21,6 +26,8 @@ export default async function ProfilePage() {
return notFound();
}

const sex = USER_SEX[profile.sex as keyof typeof USER_SEX] || "Nieznana";

return (
<div className="space-y-6">
<div>
Expand Down Expand Up @@ -54,7 +61,7 @@ export default async function ProfilePage() {
</div>
<div className="flex w-full items-center justify-between">
<p>Płeć:</p>
<h3 className="font-medium">{profile.sex.toUpperCase()}</h3>
<h3 className="font-medium">{sex}</h3>
</div>
<div className="flex w-full items-center justify-between">
<p>Numer indeksu:</p>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/plan-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export function PlanItem({
}}
>
<Download />
<span>Eksportuj do pliku .ics</span>
<span>Dodaj do kalendarza (.ics)</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
Expand Down
15 changes: 14 additions & 1 deletion frontend/src/components/user-button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { LogOut, Settings2Icon } from "lucide-react";
import { CircleHelpIcon, LogOut, Settings2Icon } from "lucide-react";
import Link from "next/link";
import React from "react";

Expand Down Expand Up @@ -56,6 +56,19 @@ export function UserButton({ profile }: { profile: GetProfile }) {
<h2 className="text-sm font-medium">Ustawienia</h2>
</button>
</Link>
<Link href="/plans/account/calendar" className="w-full">
<button
className="flex w-full items-center gap-3 border-t bg-background p-4 py-4 transition-all hover:bg-muted/50"
onClick={() => {
setOpened(false);
}}
>
<div className="mr-1 flex w-[40px] items-center justify-center">
<CircleHelpIcon className="h-4 w-4" />
</div>
<h2 className="text-sm font-medium">Jak dodać do kalendarza?</h2>
</button>
</Link>
<SignOutButton asChild={true}>
<button className="flex w-full items-center gap-3 rounded-b-lg border-b border-t bg-background p-4 py-4 shadow-sm transition-all hover:bg-muted/50 dark:hover:shadow-black/50">
<div className="mr-1 flex w-[40px] items-center justify-center">
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ import type { Registration } from "@/lib/types";
import type { Day } from "@/services/usos/types";

export interface User {
id: number;
firstName: string;
lastName: string;
studentNumber: number;
usosId: string;
createdAt: string;
updatedAt: string;
allowNotifications: boolean;
}

export interface UserSettingsPayload {
allowNotifications: boolean;
}

export interface CreatePlanResponseType {
Expand Down

0 comments on commit e199edf

Please sign in to comment.