completed user admin page
This commit is contained in:
@@ -14,7 +14,6 @@ export default async function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const session = await getServerSession();
|
||||
console.log(session);
|
||||
if (!session) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { create } from "zustand";
|
||||
import { socket } from "../(dispatch)/socket";
|
||||
|
||||
console.log("connectionStore");
|
||||
|
||||
interface ConnectionStore {
|
||||
isConnected: boolean;
|
||||
connect: (uid: string) => Promise<void>;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
API_PORT=
|
||||
MOODLE_TOKEN=
|
||||
MOODLE_URL=
|
||||
MAIL_SERVER=
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import "dotenv/config";
|
||||
import "modules/chron";
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import router from "routes/router";
|
||||
|
||||
// Add API eventually
|
||||
console.log("VAR hub Server started");
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
app.use(cors());
|
||||
|
||||
app.use(router);
|
||||
|
||||
const port = process.env.API_PORT || 3003;
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is running on port ${port}`);
|
||||
});
|
||||
|
||||
202
apps/hub-server/modules/mail-templates/PasswordChanged.tsx
Normal file
202
apps/hub-server/modules/mail-templates/PasswordChanged.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import * as React from "react";
|
||||
import { User } from "@repo/db";
|
||||
import { Html, render } from "@react-email/components";
|
||||
|
||||
const styles = `
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a[x-apple-data-detectors] {
|
||||
color: inherit !important;
|
||||
text-decoration: inherit !important;
|
||||
}
|
||||
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.desktop_hide,
|
||||
.desktop_hide table {
|
||||
mso-hide: all;
|
||||
display: none;
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image_block img + div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
sup,
|
||||
sub {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.menu_block.desktop_hide .menu-links span {
|
||||
mso-hide: all;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.desktop_hide table.icons-inner {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.icons-inner {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icons-inner td {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Template = ({ user, password }: { user: User; password: string }) => (
|
||||
<Html lang="de">
|
||||
<meta content="text/html; charset=utf-8" httpEquiv="Content-Type" />
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Montserrat"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<style>{styles}</style>
|
||||
<body style={{ backgroundColor: "#FFFFFF", margin: 0, padding: 0 }}>
|
||||
<table width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<table
|
||||
align="center"
|
||||
width="680"
|
||||
style={{ margin: "0 auto", color: "#000000" }}
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ textAlign: "center", paddingTop: "30px" }}>
|
||||
<img
|
||||
src={`${process.env.HUB_URL}/mail/var_logo.png`}
|
||||
alt="Logo"
|
||||
width="80"
|
||||
style={{ display: "block", margin: "0 auto" }}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
style={{
|
||||
textAlign: "center",
|
||||
fontSize: "46px",
|
||||
color: "#011936",
|
||||
}}
|
||||
>
|
||||
<strong>Passwort geändert</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
style={{
|
||||
textAlign: "center",
|
||||
fontSize: "30px",
|
||||
color: "#011936",
|
||||
}}
|
||||
>
|
||||
Hallo {user.firstname},
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
style={{
|
||||
textAlign: "center",
|
||||
fontSize: "18px",
|
||||
color: "#011936",
|
||||
padding: "20px",
|
||||
}}
|
||||
>
|
||||
Dein Passwort wurde erfolgreich geändert. Wenn du diese
|
||||
Änderung nicht vorgenommen hast, kontaktiere bitte sofort
|
||||
unseren Support.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
style={{
|
||||
textAlign: "center",
|
||||
fontSize: "18px",
|
||||
color: "#011936",
|
||||
padding: "20px",
|
||||
}}
|
||||
>
|
||||
Dein neues Passwort lautet: <strong>{password}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ textAlign: "center", paddingTop: "20px" }}>
|
||||
<a
|
||||
href="https://your-platform.com"
|
||||
style={{
|
||||
padding: "10px",
|
||||
textDecoration: "none",
|
||||
borderRadius: "20px",
|
||||
color: "#011936",
|
||||
}}
|
||||
>
|
||||
Impressum
|
||||
</a>
|
||||
<span style={{ margin: "0 10px" }}>|</span>
|
||||
<a
|
||||
href="https://your-platform.com"
|
||||
style={{
|
||||
padding: "10px",
|
||||
textDecoration: "none",
|
||||
borderRadius: "20px",
|
||||
color: "#011936",
|
||||
}}
|
||||
>
|
||||
Datenschutzerklärung
|
||||
</a>
|
||||
<span style={{ margin: "0 10px" }}>|</span>
|
||||
<a
|
||||
href="https://your-platform.com"
|
||||
style={{
|
||||
padding: "10px",
|
||||
textDecoration: "none",
|
||||
borderRadius: "20px",
|
||||
color: "#011936",
|
||||
}}
|
||||
>
|
||||
Knowledgebase
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
|
||||
export function renderPasswordChanged({
|
||||
user,
|
||||
password,
|
||||
}: {
|
||||
user: User;
|
||||
password: string;
|
||||
}) {
|
||||
return render(<Template user={user} password={password} />);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Event, User } from "@repo/db";
|
||||
import nodemailer from "nodemailer";
|
||||
import { renderCourseCompleted } from "./mail-templates/CourseCompleted";
|
||||
import { renderPasswordChanged } from "./mail-templates/PasswordChanged";
|
||||
|
||||
let transporter: nodemailer.Transporter | null = null;
|
||||
|
||||
@@ -42,18 +43,39 @@ export const sendCourseCompletedEmail = async (
|
||||
console.error("Transporter is not initialized");
|
||||
return;
|
||||
}
|
||||
transporter.sendMail(
|
||||
{
|
||||
from: process.env.MAIL_USER,
|
||||
to,
|
||||
subject: `Kurs ${event.name} erfolgreich abgeschlossen`,
|
||||
html: emailHtml,
|
||||
},
|
||||
(error, info) => {
|
||||
if (error) {
|
||||
console.error("Error sending email:", error);
|
||||
} else {
|
||||
}
|
||||
},
|
||||
);
|
||||
sendMail(to, `Kurs ${event.name} erfolgreich abgeschlossen`, emailHtml);
|
||||
};
|
||||
|
||||
export const sendPasswordChanged = async (
|
||||
to: string,
|
||||
user: User,
|
||||
password: string,
|
||||
) => {
|
||||
const emailHtml = await renderPasswordChanged({ user, password });
|
||||
|
||||
await sendMail(to, `Dein Passwort wurde geändert`, emailHtml);
|
||||
};
|
||||
|
||||
export const sendMail = async (to: string, subject: string, html: string) =>
|
||||
new Promise<void>(async (resolve, reject) => {
|
||||
if (!transporter) {
|
||||
console.error("Transporter is not initialized");
|
||||
return;
|
||||
}
|
||||
await transporter.sendMail(
|
||||
{
|
||||
from: process.env.MAIL_USER,
|
||||
to,
|
||||
subject: subject,
|
||||
html,
|
||||
},
|
||||
(error, info) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
console.error("Error sending email:", error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -18,8 +18,10 @@
|
||||
"dependencies": {
|
||||
"@react-email/components": "^0.0.33",
|
||||
"axios": "^1.7.9",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^4.1.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"nodemailer": "^6.10.0",
|
||||
"react": "^19.0.0"
|
||||
}
|
||||
|
||||
56
apps/hub-server/routes/mail.ts
Normal file
56
apps/hub-server/routes/mail.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Router } from "express";
|
||||
import { sendMail } from "modules/mail";
|
||||
import { sendPasswordChanged, sendCourseCompletedEmail } from "modules/mail";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/send", async (req, res) => {
|
||||
console.log(req.body);
|
||||
const { to, subject, html } = req.body;
|
||||
|
||||
try {
|
||||
await sendMail(to, subject, html);
|
||||
// Send email logic here
|
||||
res.status(200).json({ message: "Email sent successfully" });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: "Failed to send email" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/template/:template", async (req, res) => {
|
||||
const { template } = req.params;
|
||||
const { to, data } = req.body;
|
||||
if (!to || !data) {
|
||||
res.status(400).json({ error: "Missing required fields" });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.user) {
|
||||
res.status(400).json({ error: "Missing user data" });
|
||||
return;
|
||||
}
|
||||
console.log("template", template);
|
||||
switch (template) {
|
||||
case "password-change":
|
||||
if (!data.password) {
|
||||
res.status(400).json({ error: "Missing password data" });
|
||||
return;
|
||||
}
|
||||
await sendPasswordChanged(to, data.user, data.password);
|
||||
break;
|
||||
case "course-completed":
|
||||
if (!data.event) {
|
||||
res.status(400).json({ error: "Missing event data" });
|
||||
return;
|
||||
}
|
||||
await sendCourseCompletedEmail(to, data.user, data.event);
|
||||
break;
|
||||
default:
|
||||
res.status(400).json({ error: "Invalid template" });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ message: "Email sent successfully" });
|
||||
});
|
||||
|
||||
export default router;
|
||||
8
apps/hub-server/routes/router.ts
Normal file
8
apps/hub-server/routes/router.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Router } from "express";
|
||||
import mailRouter from "./mail";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use("/mail", mailRouter);
|
||||
|
||||
export default router;
|
||||
@@ -62,7 +62,6 @@ export default async () => {
|
||||
});
|
||||
|
||||
const filteredEvents = events.filter((event) => {
|
||||
console.log;
|
||||
if (eventCompleted(event, event.participants[0])) return false;
|
||||
if (
|
||||
event.type === "OBLIGATED_COURSE" &&
|
||||
|
||||
@@ -61,9 +61,8 @@ export const AppointmentModal = ({
|
||||
</h3>
|
||||
<form
|
||||
onSubmit={appointmentForm.handleSubmit(async (values) => {
|
||||
console.log(values);
|
||||
if (!event) return;
|
||||
const createdAppointment = await upsertAppointment(values);
|
||||
await upsertAppointment(values);
|
||||
ref.current?.close();
|
||||
appointmentsTableRef.current?.refresh();
|
||||
})}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { BADGES, User } from "@repo/db";
|
||||
import { BADGES, PERMISSION, User } from "@repo/db";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { updateUser } from "../../../../settings/actions";
|
||||
import { editUser, resetPassword } from "../../action";
|
||||
import { toast } from "react-hot-toast";
|
||||
import {
|
||||
PersonIcon,
|
||||
@@ -18,36 +18,25 @@ import {
|
||||
} from "@radix-ui/react-icons";
|
||||
import { Button } from "../../../../../_components/ui/Button";
|
||||
import { Select } from "../../../../../_components/ui/Select";
|
||||
import { UserSchema } from "@repo/db/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface ProfileFormProps {
|
||||
user: User | null;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export const ProfileForm: React.FC<ProfileFormProps> = ({ user }) => {
|
||||
const schema = z.object({
|
||||
firstname: z.string().min(2).max(30),
|
||||
lastname: z.string().min(2).max(30),
|
||||
email: z.string().email({
|
||||
message: "Bitte gebe eine gültige E-Mail Adresse ein",
|
||||
}),
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
type IFormInput = z.infer<typeof schema>;
|
||||
|
||||
const form = useForm<IFormInput>({
|
||||
defaultValues: {
|
||||
firstname: user?.firstname,
|
||||
lastname: user?.lastname,
|
||||
email: user?.email,
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
const form = useForm<User>({
|
||||
defaultValues: user,
|
||||
resolver: zodResolver(UserSchema),
|
||||
});
|
||||
return (
|
||||
<form
|
||||
className="card-body"
|
||||
onSubmit={form.handleSubmit(async (values) => {
|
||||
setIsLoading(true);
|
||||
await updateUser(values);
|
||||
await editUser(values.id, values);
|
||||
form.reset(values);
|
||||
setIsLoading(false);
|
||||
toast.success("Deine Änderungen wurden gespeichert!", {
|
||||
@@ -114,13 +103,23 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }) => {
|
||||
<Select
|
||||
isMulti
|
||||
form={form}
|
||||
name="finishedBadges"
|
||||
name="badges"
|
||||
label="Badges"
|
||||
options={Object.entries(BADGES).map(([key, value]) => ({
|
||||
label: value,
|
||||
value: key,
|
||||
}))}
|
||||
/>
|
||||
<Select
|
||||
isMulti
|
||||
form={form}
|
||||
name="permissions"
|
||||
label="Permissions"
|
||||
options={Object.entries(PERMISSION).map(([key, value]) => ({
|
||||
label: value,
|
||||
value: key,
|
||||
}))}
|
||||
/>
|
||||
<div className="card-actions justify-center pt-6">
|
||||
<Button
|
||||
role="submit"
|
||||
@@ -137,6 +136,8 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }) => {
|
||||
};
|
||||
|
||||
export const AdminForm: React.FC<ProfileFormProps> = ({ user }) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">
|
||||
@@ -145,23 +146,59 @@ export const AdminForm: React.FC<ProfileFormProps> = ({ user }) => {
|
||||
<div className="text-left">
|
||||
<div className="card-actions pt-6">
|
||||
<Button
|
||||
role="submit"
|
||||
onClick={async () => {
|
||||
const { password } = await resetPassword(user.id);
|
||||
toast.success(
|
||||
`Neues Passwort
|
||||
${password}, es wurde dem Nutzer an die E-Mail gesendet!`,
|
||||
{
|
||||
style: {
|
||||
background: "var(--color-base-100)",
|
||||
color: "var(--color-base-content)",
|
||||
},
|
||||
},
|
||||
);
|
||||
}}
|
||||
className="btn-sm btn-wide btn-outline btn-success"
|
||||
>
|
||||
<LockOpen1Icon /> Passwort zurücksetzen
|
||||
</Button>
|
||||
<Button
|
||||
role="submit"
|
||||
className="btn-sm btn-wide btn-outline btn-error"
|
||||
>
|
||||
<HobbyKnifeIcon /> User Sperren
|
||||
</Button>
|
||||
<Button
|
||||
role="submit"
|
||||
className="btn-sm btn-wide btn-outline btn-warning"
|
||||
>
|
||||
<HeartIcon /> User Entperren
|
||||
</Button>
|
||||
{!user.isBanned && (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await editUser(user.id, { isBanned: true });
|
||||
toast.success("Nutzer wurde gesperrt!", {
|
||||
style: {
|
||||
background: "var(--color-base-100)",
|
||||
color: "var(--color-base-content)",
|
||||
},
|
||||
});
|
||||
router.refresh();
|
||||
}}
|
||||
role="submit"
|
||||
className="btn-sm btn-wide btn-outline btn-error"
|
||||
>
|
||||
<HobbyKnifeIcon /> User Sperren
|
||||
</Button>
|
||||
)}
|
||||
{user.isBanned && (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await editUser(user.id, { isBanned: false });
|
||||
toast.success("Nutzer wurde entsperrt!", {
|
||||
style: {
|
||||
background: "var(--color-base-100)",
|
||||
color: "var(--color-base-content)",
|
||||
},
|
||||
});
|
||||
router.refresh();
|
||||
}}
|
||||
role="submit"
|
||||
className="btn-sm btn-wide btn-outline btn-warning"
|
||||
>
|
||||
<HobbyKnifeIcon /> User Entperren
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { PersonIcon } from "@radix-ui/react-icons";
|
||||
import { PrismaClient, User } from "@repo/db";
|
||||
import { AdminForm, ProfileForm } from "./_components/forms";
|
||||
import { Error } from "../../../../_components/Error";
|
||||
|
||||
export default async ({ params }: { params: { id: string } }) => {
|
||||
const prisma = new PrismaClient();
|
||||
const { id } = params;
|
||||
const { id } = await params;
|
||||
|
||||
const user: User | null = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(user);
|
||||
|
||||
if (!user) return <Error statusCode={404} title="User not found" />;
|
||||
return (
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
<div className="col-span-full">
|
||||
|
||||
35
apps/hub/app/(app)/admin/user/action.ts
Normal file
35
apps/hub/app/(app)/admin/user/action.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
"use server";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { prisma, Prisma } from "@repo/db";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { sendMailByTemplate } from "../../../../helper/mail";
|
||||
|
||||
export const editUser = async (id: string, data: Prisma.UserUpdateInput) => {
|
||||
return await prisma.user.update({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const resetPassword = async (id: string) => {
|
||||
const password = Math.random().toString(36).slice(-8);
|
||||
const hashedPassword = await bcrypt.hash(password, 15);
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
await sendMailByTemplate(user.email, "password-change", {
|
||||
user: user,
|
||||
password: password,
|
||||
});
|
||||
|
||||
return { password };
|
||||
};
|
||||
@@ -103,13 +103,6 @@ const ModalBtn = ({
|
||||
? (selectedDate as any)?.Participants?.length + 1
|
||||
: ownIndexInParticipantList + 1;
|
||||
|
||||
console.log({
|
||||
selectedDate,
|
||||
ownPlaceInParticipantList,
|
||||
ownIndexInParticipantList,
|
||||
maxParticipants: event.maxParticipants,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
|
||||
@@ -43,7 +43,7 @@ export const Register = () => {
|
||||
passwordConfirm: "",
|
||||
},
|
||||
});
|
||||
console.log(form.formState.errors);
|
||||
|
||||
return (
|
||||
<form
|
||||
className="card-body"
|
||||
|
||||
11
apps/hub/app/_components/Error.tsx
Normal file
11
apps/hub/app/_components/Error.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
export const Error = ({
|
||||
statusCode,
|
||||
title,
|
||||
}: {
|
||||
statusCode: number;
|
||||
title: string;
|
||||
}) => {
|
||||
return <Error statusCode={404} title="User not found" />;
|
||||
};
|
||||
@@ -1,25 +1,43 @@
|
||||
import { ButtonHTMLAttributes, DetailedHTMLProps } from 'react';
|
||||
import { cn } from '../../../helper/cn';
|
||||
import {
|
||||
ButtonHTMLAttributes,
|
||||
DetailedHTMLProps,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { cn } from "../../../helper/cn";
|
||||
|
||||
export const Button = ({
|
||||
isLoading,
|
||||
...props
|
||||
isLoading,
|
||||
...props
|
||||
}: DetailedHTMLProps<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
HTMLButtonElement
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
HTMLButtonElement
|
||||
> & {
|
||||
isLoading?: boolean;
|
||||
isLoading?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
{...(props as any)}
|
||||
className={cn('btn', props.className)}
|
||||
disabled={isLoading || props.disabled}
|
||||
>
|
||||
{isLoading && (
|
||||
<span className="loading loading-spinner loading-sm"></span>
|
||||
)}
|
||||
{props.children as any}
|
||||
</button>
|
||||
);
|
||||
const [isLoadingState, setIsLoadingState] = useState(isLoading);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoadingState(isLoading);
|
||||
}, [isLoading]);
|
||||
|
||||
return (
|
||||
<button
|
||||
{...(props as any)}
|
||||
className={cn("btn", props.className)}
|
||||
disabled={isLoadingState || props.disabled}
|
||||
onClick={async (e) => {
|
||||
if (props.onClick) {
|
||||
setIsLoadingState(true);
|
||||
await props.onClick(e);
|
||||
setIsLoadingState(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isLoadingState && (
|
||||
<span className="loading loading-spinner loading-sm"></span>
|
||||
)}
|
||||
{props.children as any}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -76,11 +76,16 @@ const SelectCom = <T extends FieldValues>({
|
||||
<SelectTemplate
|
||||
onChange={(newValue: any) => {
|
||||
if (Array.isArray(newValue)) {
|
||||
form.setValue(name, newValue.map((v: any) => v.value) as any);
|
||||
form.setValue(name, newValue.map((v: any) => v.value) as any, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
} else {
|
||||
form.setValue(name, newValue.value);
|
||||
form.setValue(name, newValue.value, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}
|
||||
form.trigger(name);
|
||||
form.Dirty;
|
||||
}}
|
||||
value={
|
||||
(inputProps as any)?.isMulti
|
||||
|
||||
@@ -24,9 +24,7 @@ export const GET = async (req: NextRequest) => {
|
||||
if (!user)
|
||||
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
||||
setTimeout(async () => {
|
||||
console.log("getting moodle ID");
|
||||
const moodleUser = await getMoodleUserById(user.id);
|
||||
console.log("got moodle ID", moodleUser.id);
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
|
||||
40
apps/hub/helper/mail.ts
Normal file
40
apps/hub/helper/mail.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export const sendMail = async (
|
||||
email: string,
|
||||
subject: string,
|
||||
html: string,
|
||||
) => {
|
||||
await fetch(`${process.env.NEXT_PUBLIC_HUB_SERVER_URL}/mail/send`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
body: JSON.stringify({
|
||||
to: email,
|
||||
subject,
|
||||
html,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export const sendMailByTemplate = async (
|
||||
email: string,
|
||||
template: "password-change" | "course-completed",
|
||||
data: any,
|
||||
) => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_HUB_SERVER_URL}/mail/template/${template}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
body: JSON.stringify({
|
||||
to: email,
|
||||
data,
|
||||
}),
|
||||
},
|
||||
);
|
||||
console.log(res);
|
||||
} catch (error) {
|
||||
console.error("Error sending mail:", error);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user