diff --git a/core/API.md b/core/API.md index d7b56d35b5..457585e3bf 100644 --- a/core/API.md +++ b/core/API.md @@ -90,7 +90,8 @@ params: - api key - list of fields? - template (should be able to reference user fields which will be substituted with values for the recipient. and pubs too, if that pub is in the instance scope) - user vars: - user.id - - user.name + - user.firstName + - user.lastName - user.email - user.JWT (useful for generating magic link) - instance vars: diff --git a/core/app/(user)/settings/SettingsForm.tsx b/core/app/(user)/settings/SettingsForm.tsx index e5bb3bc079..1c6ba5cf0a 100644 --- a/core/app/(user)/settings/SettingsForm.tsx +++ b/core/app/(user)/settings/SettingsForm.tsx @@ -1,69 +1,74 @@ "use client"; import React, { useState, FormEvent } from "react"; import { Button } from "ui"; -import { UserPutBody } from "app/api/user/route"; import { supabase } from "lib/supabase"; import { useRouter } from "next/navigation"; import { getSlugSuffix, slugifyString } from "lib/string"; -type Props = { - name: string; - email: string; - slug: string; -}; +import { UserPutBody, UserSettings } from "~/lib/types"; -export default function SettingsForm({ name: initName, email: initEmail, slug }: Props) { - const [name, setName] = useState(initName); +type Props = UserSettings; + +export default function SettingsForm({ + firstName: initFirstName, + lastName: initLastName, + email: initEmail, + slug, +}: Props) { + const [firstName, setFirstName] = useState(initFirstName); + const [lastName, setLastName] = useState(initLastName); const [email, setEmail] = useState(initEmail); - const [emailError, setEmailError] = useState('') + const [emailError, setEmailError] = useState(""); const [emailIsLoading, setEmailIsLoading] = useState(false); - const [emailSuccess, setEmailSuccess] = useState(false) - const [isLoading, setIsLoading] = useState(false); - const [resetIsLoading, setResetIsLoading] = useState(false); + const [emailSuccess, setEmailSuccess] = useState(false); + const [, setIsLoading] = useState(false); + const [, setResetIsLoading] = useState(false); const [resetSuccess, setResetSuccess] = useState(false); const emailChanged = initEmail !== email; const router = useRouter(); - const valuesChanged = name !== initName; + const valuesChanged = emailChanged || firstName !== initFirstName || lastName !== initLastName; const slugSuffix = getSlugSuffix(slug); const updateEmail = async (e: FormEvent) => { e.preventDefault(); - setEmailError("") + setEmailError(""); if (emailChanged) { setEmailIsLoading(true); const response = await fetch("/api/user?email=" + email, { method: "GET", headers: { "content-type": "application/json" }, - }) - const genericError = () => setEmailError('An error happened while trying to update your email') + }); + const genericError = () => + setEmailError("An error happened while trying to update your email"); if (!response.ok) { if (response.status === 403) { setEmailError(`A PubPub account already exists for ${email}`); } else { - genericError() - const { message }: { message?: string } = await response.json() + genericError(); + const { message }: { message?: string } = await response.json(); console.error(`Error: ${response.status} ${message}`); } - setEmailIsLoading(false) - return + setEmailIsLoading(false); + return; } const { error } = await supabase.auth.updateUser({ email }); setEmailIsLoading(false); if (error) { - genericError() + genericError(); console.error(error); } setEmailSuccess(true); } - } + }; const handleSubmit = async (evt: FormEvent) => { evt.preventDefault(); setIsLoading(true); let putBody: UserPutBody = { - name, + firstName, + lastName, }; const response = await fetch("/api/user", { method: "PUT", @@ -74,7 +79,7 @@ export default function SettingsForm({ name: initName, email: initEmail, slug }: setIsLoading(false); if (!response.ok) { if (data.message) { - console.error(data.message) + console.error(data.message); } } else { router.refresh(); @@ -97,16 +102,26 @@ export default function SettingsForm({ name: initName, email: initEmail, slug }: <>
- - setName(evt.target.value)} /> -
- Username: {slugifyString(name)}-{slugSuffix} +
+ + setFirstName(evt.target.value)} + className="mr-2" + /> + + setLastName(evt.target.value)} + />
- @@ -120,20 +135,21 @@ export default function SettingsForm({ name: initName, email: initEmail, slug }: /> - {!emailIsLoading && (emailError ? ( -
- {emailError} -
- ) : emailSuccess && ( -
- You will need to confirm this change by clicking a link sent to the new - email address. -
- ))} + {!emailIsLoading && + (emailError ? ( +
{emailError}
+ ) : ( + emailSuccess && ( +
+ You will need to confirm this change by clicking a link sent to + the new email address. +
+ ) + ))}

Click below to receive an email with a secure link for reseting yor password. diff --git a/core/app/(user)/settings/page.tsx b/core/app/(user)/settings/page.tsx index dcc7f4b4ba..8d748fc652 100644 --- a/core/app/(user)/settings/page.tsx +++ b/core/app/(user)/settings/page.tsx @@ -9,7 +9,12 @@ export default async function Page() { } return (

- +
); } diff --git a/core/app/(user)/signup/SignupForm.tsx b/core/app/(user)/signup/SignupForm.tsx index cd3a75aa5d..7f4506ae11 100644 --- a/core/app/(user)/signup/SignupForm.tsx +++ b/core/app/(user)/signup/SignupForm.tsx @@ -1,10 +1,11 @@ "use client"; import React, { useState, FormEvent } from "react"; import { Button } from "ui"; -import { UserPostBody } from "app/api/user/route"; +import { UserPostBody } from "~/lib/types"; export default function SignupForm() { - const [name, setName] = useState(""); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); const [password, setPassword] = useState(""); const [email, setEmail] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -15,7 +16,8 @@ export default function SignupForm() { setIsLoading(true); const postBody: UserPostBody = { - name, + firstName, + lastName, password, email, }; @@ -42,15 +44,27 @@ export default function SignupForm() {
- +
setName(evt.target.value)} + name="firstName" + value={firstName} + onChange={(evt) => setFirstName(evt.target.value)} + /> +
+
+ +
+
+ setLastName(evt.target.value)} />
@@ -82,7 +96,7 @@ export default function SignupForm() { diff --git a/core/app/api/user/route.ts b/core/app/api/user/route.ts index d139a17106..e49ae216e7 100644 --- a/core/app/api/user/route.ts +++ b/core/app/api/user/route.ts @@ -7,19 +7,21 @@ import { getLoginId } from "lib/auth/loginId"; import { BadRequestError, ForbiddenError, UnauthorizedError, handleErrors } from "~/lib/server"; export type UserPostBody = { - name: string; + firstName: string; + lastName?: string; email: string; password: string; }; export type UserPutBody = { - name: string; + firstName: string; + lastName?: string; }; export async function POST(req: NextRequest) { return await handleErrors(async () => { const submittedData: UserPostBody = await req.json(); - const { name, email, password } = submittedData; + const { firstName, lastName, email, password } = submittedData; const supabase = getServerSupabase(); const { data, error } = await supabase.auth.signUp({ email, @@ -54,29 +56,32 @@ export async function POST(req: NextRequest) { await prisma.user.create({ data: { id: data.user.id, - slug: `${slugifyString(name)}-${generateHash(4, "0123456789")}`, - name, + slug: `${slugifyString(firstName)}${ + lastName ? `-${slugifyString(lastName)}` : "" + }-${generateHash(4, "0123456789")}`, + firstName, + lastName: lastName || undefined, email, }, }); return NextResponse.json({}, { status: 201 }); - }) + }); } export async function PUT(req: NextRequest) { return await handleErrors(async () => { const loginId = await getLoginId(req); if (!loginId) { - throw new UnauthorizedError() + throw new UnauthorizedError(); } const submittedData: UserPutBody = await req.json(); - const { name } = submittedData; + const { firstName, lastName } = submittedData; const currentData = await prisma.user.findUnique({ where: { id: loginId }, }); if (!currentData) { - throw new BadRequestError('Unable to find user') + throw new BadRequestError("Unable to find user"); } const slugSuffix = getSlugSuffix(currentData.slug); await prisma.user.update({ @@ -84,12 +89,13 @@ export async function PUT(req: NextRequest) { id: loginId, }, data: { - slug: `${slugifyString(name)}-${slugSuffix}`, - name, + slug: `${slugifyString(firstName)}-${slugSuffix}`, + firstName, + lastName, }, }); return NextResponse.json({}, { status: 200 }); - }) + }); } // Used to determine if an email is available when a user attempts to change theirs @@ -97,25 +103,25 @@ export async function GET(req: NextRequest) { return await handleErrors(async () => { const loginId = await getLoginId(req); if (!loginId) { - throw new UnauthorizedError() + throw new UnauthorizedError(); } - const email = req.nextUrl.searchParams.get('email') + const email = req.nextUrl.searchParams.get("email"); if (!email) { - throw new BadRequestError() + throw new BadRequestError(); } const emailUsed = await prisma.user.findUnique({ where: { - email - } - }) + email, + }, + }); if (emailUsed) { - throw new ForbiddenError('Email already in use') + throw new ForbiddenError("Email already in use"); } - return NextResponse.json({message: "Email is available"}, { status: 200 }) - }) + return NextResponse.json({ message: "Email is available" }, { status: 200 }); + }); } diff --git a/core/app/c/[communitySlug]/LoginSwitcher.tsx b/core/app/c/[communitySlug]/LoginSwitcher.tsx index 7a6c87a08e..addc3716f5 100644 --- a/core/app/c/[communitySlug]/LoginSwitcher.tsx +++ b/core/app/c/[communitySlug]/LoginSwitcher.tsx @@ -1,6 +1,7 @@ import { getLoginData } from "~/lib/auth/loginData"; import { Avatar, AvatarFallback, AvatarImage } from "ui"; import LogoutButton from "./LogoutButton"; +import Link from "next/link"; export default async function LoginSwitcher() { const loginData = await getLoginData(); @@ -8,19 +9,18 @@ export default async function LoginSwitcher() { return null; } return ( -
+
- - {loginData.name[0]} + + + {loginData.firstName[0]} + -
-
{loginData.name}
+
+
{loginData.firstName}
{loginData.email}
-
- -
); } diff --git a/core/app/c/[communitySlug]/pubs/[pubId]/page.tsx b/core/app/c/[communitySlug]/pubs/[pubId]/page.tsx index e5fbb277c1..c49e3e5364 100644 --- a/core/app/c/[communitySlug]/pubs/[pubId]/page.tsx +++ b/core/app/c/[communitySlug]/pubs/[pubId]/page.tsx @@ -89,7 +89,7 @@ export default async function Page({
- {user.name[0]} + {user.firstName[0]}
); diff --git a/core/app/c/[communitySlug]/stages/components/Assign.tsx b/core/app/c/[communitySlug]/stages/components/Assign.tsx index c10fca0d45..eaff45b1f8 100644 --- a/core/app/c/[communitySlug]/stages/components/Assign.tsx +++ b/core/app/c/[communitySlug]/stages/components/Assign.tsx @@ -20,7 +20,7 @@ import { PubPayload, StagePayload, StagePayloadMoveConstraintDestination, - User, + UserLoginData, } from "~/lib/types"; import { assign } from "./lib/actions"; @@ -28,7 +28,7 @@ type Props = { pub: PubPayload; stages: StagePayloadMoveConstraintDestination[]; stage: StagePayload; - loginData: User; + loginData: UserLoginData; users: PermissionPayloadUser[]; }; @@ -86,23 +86,28 @@ export default function Assign(props: Props) { - Assign {getTitle(props.pub)} to {user.name}? + Assign {getTitle(props.pub)} to {user.firstName}{" "} + {user.lastName}? - {user.name} will be notified that they have been assigned to - this Pub. + {user.firstName} {user.lastName} will be notified that they + have been assigned to this Pub. + + + + + + + + ); +} diff --git a/integrations/evaluations/app/actions/manage/EvaluatorInviteRow.tsx b/integrations/evaluations/app/actions/manage/EvaluatorInviteRow.tsx new file mode 100644 index 0000000000..02f06774d1 --- /dev/null +++ b/integrations/evaluations/app/actions/manage/EvaluatorInviteRow.tsx @@ -0,0 +1,210 @@ +"use client"; + +import { SuggestedMembersQuery } from "@pubpub/sdk"; +import { memo, useEffect, useState } from "react"; +import { Control, useWatch } from "react-hook-form"; +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + Icon, + Input, + Textarea, +} from "ui"; +import * as z from "zod"; +import { EvaluatorSuggestButton } from "./EvaluatorSuggestButton"; +import { EmailFormSchema } from "./types"; + +const pad = (n: number) => (n < 10 ? "0" + n : n); +const daysHoursMinutes = (ms: number) => { + const msInHour = 60 * 60 * 1000; + const msInDay = 24 * msInHour; + let days = Math.floor(ms / msInDay); + let hours = Math.floor((ms - days * msInDay) / msInHour); + let minutes = Math.round((ms - days * msInDay - hours * msInHour) / 60000); + if (minutes === 60) { + hours++; + minutes = 0; + } + if (hours === 24) { + days++; + hours = 0; + } + return [days, pad(hours), pad(minutes)].join(":"); +}; + +type Props = { + control: Control; + inviteTime: string | undefined; + index: number; + onRemove: (index: number) => void; + onSuggest: (index: number, query: SuggestedMembersQuery) => void; +}; + +export const EvaluatorInviteRow = memo((props: Props) => { + const getTimeBeforeInviteSent = () => + props.inviteTime ? new Date(props.inviteTime).getTime() - Date.now() : Infinity; + const [timeBeforeInviteSent, setTimeBeforeInviteSent] = useState(getTimeBeforeInviteSent); + const invite = useWatch>({ + control: props.control, + name: `invites.${props.index}`, + }); + const inviteSent = timeBeforeInviteSent <= 0; + const inviteHasUser = typeof invite === "object" && "userId" in invite; + + // Update the timer every second. + useEffect(() => { + let interval: NodeJS.Timeout; + if (timeBeforeInviteSent > 0) { + interval = setInterval(() => { + const timeBeforeInviteSent = getTimeBeforeInviteSent(); + setTimeBeforeInviteSent(timeBeforeInviteSent); + // Clear the timer when the invite would be sent. + if (timeBeforeInviteSent <= 0) clearInterval(interval); + }, 1000); + } + return () => clearInterval(interval); + }, [props.inviteTime]); + + return ( +
+ ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> +
+ props.onSuggest(props.index, invite as SuggestedMembersQuery)} + /> + + + + + + + Edit Template + {props.inviteTime && + (inviteSent ? ( + + This email was sent at{" "} + + {new Date(props.inviteTime).toLocaleString()} + + , and can no longer be edited. + + ) : ( + + This email is scheduled to be sent at{" "} + + {new Date(props.inviteTime).toLocaleString()} + + . + + ))} + +
+ ( + + Subject + + + + + + )} + /> + ( + + Email Message + +