mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-05 21:26:05 +02:00
♻️ Use server side session API (#1684)
This commit is contained in:
parent
aba602ef89
commit
5c61057a84
17 changed files with 219 additions and 235 deletions
|
@ -1,9 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { usePostHog } from "@rallly/posthog/client";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Logo } from "@/components/logo";
|
import { Logo } from "@/components/logo";
|
||||||
|
@ -16,8 +14,6 @@ import { trpc } from "@/trpc/client";
|
||||||
type PageProps = { magicLink: string; email: string };
|
type PageProps = { magicLink: string; email: string };
|
||||||
|
|
||||||
export const LoginPage = ({ magicLink, email }: PageProps) => {
|
export const LoginPage = ({ magicLink, email }: PageProps) => {
|
||||||
const session = useSession();
|
|
||||||
const posthog = usePostHog();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
@ -28,15 +24,6 @@ export const LoginPage = ({ magicLink, email }: PageProps) => {
|
||||||
},
|
},
|
||||||
onSuccess: async (data) => {
|
onSuccess: async (data) => {
|
||||||
if (!data.url.includes("auth/error")) {
|
if (!data.url.includes("auth/error")) {
|
||||||
// if login was successful, update the session
|
|
||||||
const updatedSession = await session.update();
|
|
||||||
if (updatedSession?.user) {
|
|
||||||
// identify the user in posthog
|
|
||||||
posthog?.identify(updatedSession.user.id, {
|
|
||||||
email: updatedSession.user.email,
|
|
||||||
name: updatedSession.user.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
router.push(data.url);
|
router.push(data.url);
|
||||||
} else {
|
} else {
|
||||||
setError(
|
setError(
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { usePostHog } from "@rallly/posthog/client";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
|
@ -37,12 +36,11 @@ export function OTPForm({ token }: { token: string }) {
|
||||||
resolver: zodResolver(otpFormSchema),
|
resolver: zodResolver(otpFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { timeZone } = useDayjs();
|
const { timeZone, weekStart, timeFormat } = useDayjs();
|
||||||
|
|
||||||
const locale = i18n.language;
|
const locale = i18n.language;
|
||||||
|
|
||||||
const queryClient = trpc.useUtils();
|
const queryClient = trpc.useUtils();
|
||||||
const posthog = usePostHog();
|
|
||||||
const authenticateRegistration =
|
const authenticateRegistration =
|
||||||
trpc.auth.authenticateRegistration.useMutation();
|
trpc.auth.authenticateRegistration.useMutation();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
@ -52,6 +50,8 @@ export function OTPForm({ token }: { token: string }) {
|
||||||
token,
|
token,
|
||||||
timeZone,
|
timeZone,
|
||||||
locale,
|
locale,
|
||||||
|
weekStart,
|
||||||
|
timeFormat,
|
||||||
code: data.otp,
|
code: data.otp,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -64,11 +64,6 @@ export function OTPForm({ token }: { token: string }) {
|
||||||
|
|
||||||
queryClient.invalidate();
|
queryClient.invalidate();
|
||||||
|
|
||||||
posthog?.identify(res.user.id, {
|
|
||||||
email: res.user.email,
|
|
||||||
name: res.user.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
signIn("registration-token", {
|
signIn("registration-token", {
|
||||||
token,
|
token,
|
||||||
redirectTo: searchParams?.get("redirectTo") ?? "/",
|
redirectTo: searchParams?.get("redirectTo") ?? "/",
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { prisma } from "@rallly/database";
|
||||||
|
|
||||||
|
import { getUserId } from "@/next-auth";
|
||||||
|
|
||||||
|
export async function updateLocale(locale: string) {
|
||||||
|
const userId = await getUserId();
|
||||||
|
await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
locale,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -27,8 +27,8 @@ const formSchema = z.object({
|
||||||
type FormData = z.infer<typeof formSchema>;
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
const DateTimePreferencesForm = () => {
|
const DateTimePreferencesForm = () => {
|
||||||
const { timeFormat, weekStart, timeZone, locale } = useDayjs();
|
const { timeFormat, weekStart, timeZone } = useDayjs();
|
||||||
const { preferences, updatePreferences } = usePreferences();
|
const { updatePreferences } = usePreferences();
|
||||||
|
|
||||||
const form = useForm<FormData>({
|
const form = useForm<FormData>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
|
@ -126,25 +126,6 @@ const DateTimePreferencesForm = () => {
|
||||||
>
|
>
|
||||||
<Trans i18nKey="save" />
|
<Trans i18nKey="save" />
|
||||||
</Button>
|
</Button>
|
||||||
{preferences.timeFormat || preferences.weekStart ? (
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
updatePreferences({
|
|
||||||
weekStart: null,
|
|
||||||
timeFormat: null,
|
|
||||||
});
|
|
||||||
form.reset({
|
|
||||||
weekStart: locale.weekStart,
|
|
||||||
timeFormat: locale.timeFormat,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trans
|
|
||||||
defaults="Use locale defaults"
|
|
||||||
i18nKey="useLocaleDefaults"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
|
@ -8,9 +8,10 @@ import { z } from "zod";
|
||||||
|
|
||||||
import { LanguageSelect } from "@/components/poll/language-selector";
|
import { LanguageSelect } from "@/components/poll/language-selector";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { usePreferences } from "@/contexts/preferences";
|
|
||||||
import { useTranslation } from "@/i18n/client";
|
import { useTranslation } from "@/i18n/client";
|
||||||
|
|
||||||
|
import { updateLocale } from "../actions";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
language: z.string(),
|
language: z.string(),
|
||||||
});
|
});
|
||||||
|
@ -25,13 +26,12 @@ export const LanguagePreference = () => {
|
||||||
},
|
},
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
});
|
});
|
||||||
const { updatePreferences } = usePreferences();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(async (data) => {
|
onSubmit={form.handleSubmit(async (data) => {
|
||||||
await updatePreferences({ locale: data.language });
|
await updateLocale(data.language);
|
||||||
i18n.changeLanguage(data.language);
|
i18n.changeLanguage(data.language);
|
||||||
form.reset({ language: data.language });
|
form.reset({ language: data.language });
|
||||||
})}
|
})}
|
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
|
|
||||||
import { DateTimePreferences } from "../components/date-time-preferences";
|
|
||||||
import { LanguagePreference } from "../components/language-preference";
|
|
||||||
import {
|
import {
|
||||||
SettingsContent,
|
SettingsContent,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
} from "../components/settings-layout";
|
} from "../components/settings-layout";
|
||||||
|
import { DateTimePreferences } from "./components/date-time-preferences";
|
||||||
|
import { LanguagePreference } from "./components/language-preference";
|
||||||
|
|
||||||
export function PreferencesPage() {
|
export function PreferencesPage() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -8,11 +8,11 @@ import { TooltipProvider } from "@rallly/ui/tooltip";
|
||||||
import { domAnimation, LazyMotion } from "motion/react";
|
import { domAnimation, LazyMotion } from "motion/react";
|
||||||
import type { Viewport } from "next";
|
import type { Viewport } from "next";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import { SessionProvider } from "next-auth/react";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { TimeZoneChangeDetector } from "@/app/[locale]/timezone-change-detector";
|
import { TimeZoneChangeDetector } from "@/app/[locale]/timezone-change-detector";
|
||||||
import { UserProvider } from "@/components/user-provider";
|
import { UserProvider } from "@/components/user-provider";
|
||||||
|
import { PreferencesProvider } from "@/contexts/preferences";
|
||||||
import { getUser } from "@/data/get-user";
|
import { getUser } from "@/data/get-user";
|
||||||
import { TimezoneProvider } from "@/features/timezone/client/context";
|
import { TimezoneProvider } from "@/features/timezone/client/context";
|
||||||
import { I18nProvider } from "@/i18n/client";
|
import { I18nProvider } from "@/i18n/client";
|
||||||
|
@ -33,32 +33,26 @@ export const viewport: Viewport = {
|
||||||
initialScale: 1,
|
initialScale: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function loadLocale() {
|
|
||||||
let locale = getLocale();
|
|
||||||
|
|
||||||
const userId = await getUserId();
|
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
const user = await getUser();
|
|
||||||
if (user.locale) {
|
|
||||||
locale = user.locale;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!supportedLngs.includes(locale)) {
|
|
||||||
return defaultLocale;
|
|
||||||
}
|
|
||||||
|
|
||||||
return locale;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function Root({
|
export default async function Root({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
const locale = await loadLocale();
|
|
||||||
|
let locale = getLocale();
|
||||||
|
|
||||||
|
const userId = await getUserId();
|
||||||
|
|
||||||
|
const user = userId ? await getUser() : null;
|
||||||
|
|
||||||
|
if (user?.locale) {
|
||||||
|
locale = user.locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supportedLngs.includes(locale)) {
|
||||||
|
locale = defaultLocale;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale} className={inter.className}>
|
<html lang={locale} className={inter.className}>
|
||||||
|
@ -67,23 +61,48 @@ export default async function Root({
|
||||||
<I18nProvider locale={locale}>
|
<I18nProvider locale={locale}>
|
||||||
<TRPCProvider>
|
<TRPCProvider>
|
||||||
<LazyMotion features={domAnimation}>
|
<LazyMotion features={domAnimation}>
|
||||||
<SessionProvider session={session}>
|
|
||||||
<PostHogProvider>
|
<PostHogProvider>
|
||||||
<PostHogPageView />
|
<PostHogPageView />
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<UserProvider>
|
<UserProvider
|
||||||
<TimezoneProvider
|
user={
|
||||||
initialTimezone={session?.user?.timeZone ?? undefined}
|
user
|
||||||
|
? {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
tier: user
|
||||||
|
? user.isPro
|
||||||
|
? "pro"
|
||||||
|
: "hobby"
|
||||||
|
: "guest",
|
||||||
|
image: user.image,
|
||||||
|
}
|
||||||
|
: session?.user
|
||||||
|
? {
|
||||||
|
id: session.user.id,
|
||||||
|
tier: "guest",
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
|
<PreferencesProvider
|
||||||
|
initialValue={{
|
||||||
|
timeFormat: user?.timeFormat,
|
||||||
|
timeZone: user?.timeZone,
|
||||||
|
weekStart: user?.weekStart,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TimezoneProvider initialTimezone={user?.timeZone}>
|
||||||
<ConnectedDayjsProvider>
|
<ConnectedDayjsProvider>
|
||||||
{children}
|
{children}
|
||||||
<TimeZoneChangeDetector />
|
<TimeZoneChangeDetector />
|
||||||
</ConnectedDayjsProvider>
|
</ConnectedDayjsProvider>
|
||||||
</TimezoneProvider>
|
</TimezoneProvider>
|
||||||
|
</PreferencesProvider>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</PostHogProvider>
|
</PostHogProvider>
|
||||||
</SessionProvider>
|
|
||||||
</LazyMotion>
|
</LazyMotion>
|
||||||
</TRPCProvider>
|
</TRPCProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
import { LanguageSelect } from "@/components/poll/language-selector";
|
|
||||||
import { usePreferences } from "@/contexts/preferences";
|
|
||||||
import { useTranslation } from "@/i18n/client";
|
|
||||||
|
|
||||||
export function UserLanguageSwitcher() {
|
|
||||||
const { i18n } = useTranslation();
|
|
||||||
const { preferences, updatePreferences } = usePreferences();
|
|
||||||
const router = useRouter();
|
|
||||||
return (
|
|
||||||
<LanguageSelect
|
|
||||||
value={preferences.locale ?? i18n.language}
|
|
||||||
onChange={async (language) => {
|
|
||||||
await updatePreferences({ locale: language });
|
|
||||||
router.refresh();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -11,7 +11,6 @@ import {
|
||||||
import { Form } from "@rallly/ui/form";
|
import { Form } from "@rallly/ui/form";
|
||||||
import { useToast } from "@rallly/ui/hooks/use-toast";
|
import { useToast } from "@rallly/ui/hooks/use-toast";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { signIn, useSession } from "next-auth/react";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import useFormPersist from "react-hook-form-persist";
|
import useFormPersist from "react-hook-form-persist";
|
||||||
|
@ -42,9 +41,8 @@ export interface CreatePollPageProps {
|
||||||
|
|
||||||
export const CreatePoll: React.FunctionComponent = () => {
|
export const CreatePoll: React.FunctionComponent = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { user } = useUser();
|
const { user, createGuestIfNeeded } = useUser();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const session = useSession();
|
|
||||||
const form = useForm<NewEventData>({
|
const form = useForm<NewEventData>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: "",
|
title: "",
|
||||||
|
@ -84,11 +82,7 @@ export const CreatePoll: React.FunctionComponent = () => {
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(async (formData) => {
|
onSubmit={form.handleSubmit(async (formData) => {
|
||||||
const title = required(formData?.title);
|
const title = required(formData?.title);
|
||||||
if (session.status !== "authenticated") {
|
await createGuestIfNeeded();
|
||||||
await signIn("guest", {
|
|
||||||
redirect: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await createPoll.mutateAsync(
|
await createPoll.mutateAsync(
|
||||||
{
|
{
|
||||||
title: title,
|
title: title,
|
||||||
|
|
|
@ -26,7 +26,6 @@ import {
|
||||||
MoreHorizontalIcon,
|
MoreHorizontalIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { signIn, useSession } from "next-auth/react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
@ -57,8 +56,7 @@ function NewCommentForm({
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const poll = usePoll();
|
const poll = usePoll();
|
||||||
const { user } = useUser();
|
const { user, createGuestIfNeeded } = useUser();
|
||||||
const session = useSession();
|
|
||||||
const { participants } = useParticipants();
|
const { participants } = useParticipants();
|
||||||
|
|
||||||
const authorName = React.useMemo(() => {
|
const authorName = React.useMemo(() => {
|
||||||
|
@ -98,11 +96,7 @@ function NewCommentForm({
|
||||||
<form
|
<form
|
||||||
className="w-full space-y-2.5"
|
className="w-full space-y-2.5"
|
||||||
onSubmit={handleSubmit(async ({ authorName, content }) => {
|
onSubmit={handleSubmit(async ({ authorName, content }) => {
|
||||||
if (session.status !== "authenticated") {
|
await createGuestIfNeeded();
|
||||||
await signIn("guest", {
|
|
||||||
redirect: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await addComment.mutateAsync({ authorName, content, pollId });
|
await addComment.mutateAsync({ authorName, content, pollId });
|
||||||
reset({ authorName, content: "" });
|
reset({ authorName, content: "" });
|
||||||
onSubmit?.();
|
onSubmit?.();
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { FormMessage } from "@rallly/ui/form";
|
||||||
import { Input } from "@rallly/ui/input";
|
import { Input } from "@rallly/ui/input";
|
||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
import { TRPCClientError } from "@trpc/client";
|
import { TRPCClientError } from "@trpc/client";
|
||||||
import { signIn, useSession } from "next-auth/react";
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
|
@ -91,8 +90,7 @@ export const NewParticipantForm = (props: NewParticipantModalProps) => {
|
||||||
|
|
||||||
const isEmailRequired = poll.requireParticipantEmail;
|
const isEmailRequired = poll.requireParticipantEmail;
|
||||||
|
|
||||||
const { user } = useUser();
|
const { user, createGuestIfNeeded } = useUser();
|
||||||
const session = useSession();
|
|
||||||
const isLoggedIn = !user.isGuest;
|
const isLoggedIn = !user.isGuest;
|
||||||
const { register, setError, formState, handleSubmit } =
|
const { register, setError, formState, handleSubmit } =
|
||||||
useForm<NewParticipantFormData>({
|
useForm<NewParticipantFormData>({
|
||||||
|
@ -113,11 +111,7 @@ export const NewParticipantForm = (props: NewParticipantModalProps) => {
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(async (data) => {
|
onSubmit={handleSubmit(async (data) => {
|
||||||
try {
|
try {
|
||||||
if (session.status !== "authenticated") {
|
await createGuestIfNeeded();
|
||||||
await signIn("guest", {
|
|
||||||
redirect: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const newParticipant = await addParticipant.mutateAsync({
|
const newParticipant = await addParticipant.mutateAsync({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
votes: props.votes,
|
votes: props.votes,
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { usePostHog } from "@rallly/posthog/client";
|
import { usePostHog } from "@rallly/posthog/client";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import type { Session } from "next-auth";
|
import { signIn, signOut } from "next-auth/react";
|
||||||
import { signOut, useSession } from "next-auth/react";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useSubscription } from "@/contexts/plan";
|
import { useSubscription } from "@/contexts/plan";
|
||||||
import { PreferencesProvider } from "@/contexts/preferences";
|
|
||||||
import { useTranslation } from "@/i18n/client";
|
import { useTranslation } from "@/i18n/client";
|
||||||
import { trpc } from "@/trpc/client";
|
|
||||||
import { isOwner } from "@/utils/permissions";
|
import { isOwner } from "@/utils/permissions";
|
||||||
|
|
||||||
import { useRequiredContext } from "./use-required-context";
|
import { useRequiredContext } from "./use-required-context";
|
||||||
|
@ -16,23 +13,20 @@ import { useRequiredContext } from "./use-required-context";
|
||||||
type UserData = {
|
type UserData = {
|
||||||
id?: string;
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
email?: string | null;
|
email?: string;
|
||||||
isGuest: boolean;
|
isGuest: boolean;
|
||||||
tier: "guest" | "hobby" | "pro";
|
tier: "guest" | "hobby" | "pro";
|
||||||
timeZone?: string | null;
|
image?: string;
|
||||||
timeFormat?: "hours12" | "hours24" | null;
|
|
||||||
weekStart?: number | null;
|
|
||||||
image?: string | null;
|
|
||||||
locale?: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserContext = React.createContext<{
|
export const UserContext = React.createContext<{
|
||||||
user: UserData;
|
user: UserData;
|
||||||
refresh: (data?: Record<string, unknown>) => Promise<Session | null>;
|
refresh: () => void;
|
||||||
ownsObject: (obj: {
|
ownsObject: (obj: {
|
||||||
userId?: string | null;
|
userId?: string | null;
|
||||||
guestId?: string | null;
|
guestId?: string | null;
|
||||||
}) => boolean;
|
}) => boolean;
|
||||||
|
createGuestIfNeeded: () => Promise<void>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
|
@ -58,16 +52,37 @@ export const IfGuest = (props: { children?: React.ReactNode }) => {
|
||||||
return <>{props.children}</>;
|
return <>{props.children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserProvider = (props: { children?: React.ReactNode }) => {
|
type BaseUser = {
|
||||||
const session = useSession();
|
id: string;
|
||||||
const user = session.data?.user;
|
tier: "guest" | "hobby" | "pro";
|
||||||
|
image?: string;
|
||||||
|
name?: string;
|
||||||
|
email?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RegisteredUser = BaseUser & {
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
tier: "hobby" | "pro";
|
||||||
|
};
|
||||||
|
|
||||||
|
type GuestUser = BaseUser & {
|
||||||
|
tier: "guest";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserProvider = ({
|
||||||
|
children,
|
||||||
|
user,
|
||||||
|
}: {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
user?: RegisteredUser | GuestUser;
|
||||||
|
}) => {
|
||||||
const subscription = useSubscription();
|
const subscription = useSubscription();
|
||||||
const updatePreferences = trpc.user.updatePreferences.useMutation();
|
const { t } = useTranslation();
|
||||||
const { t, i18n } = useTranslation();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const posthog = usePostHog();
|
const posthog = usePostHog();
|
||||||
|
|
||||||
const isGuest = !user?.email;
|
const isGuest = !user || user.tier === "guest";
|
||||||
const tier = isGuest ? "guest" : subscription?.active ? "pro" : "hobby";
|
const tier = isGuest ? "guest" : subscription?.active ? "pro" : "hobby";
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -76,9 +91,7 @@ export const UserProvider = (props: { children?: React.ReactNode }) => {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
tier,
|
tier,
|
||||||
timeZone: user.timeZone ?? null,
|
image: user.image,
|
||||||
image: user.image ?? null,
|
|
||||||
locale: user.locale ?? i18n.language,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
@ -90,17 +103,20 @@ export const UserProvider = (props: { children?: React.ReactNode }) => {
|
||||||
user: {
|
user: {
|
||||||
id: user?.id,
|
id: user?.id,
|
||||||
name: user?.name ?? t("guest"),
|
name: user?.name ?? t("guest"),
|
||||||
email: user?.email || null,
|
email: user?.email,
|
||||||
isGuest,
|
isGuest,
|
||||||
tier,
|
tier,
|
||||||
timeZone: user?.timeZone ?? null,
|
image: user?.image,
|
||||||
image: user?.image ?? null,
|
|
||||||
locale: user?.locale ?? i18n.language,
|
|
||||||
},
|
},
|
||||||
refresh: async (data) => {
|
createGuestIfNeeded: async () => {
|
||||||
|
if (!user) {
|
||||||
|
await signIn("guest", {
|
||||||
|
redirect: false,
|
||||||
|
});
|
||||||
router.refresh();
|
router.refresh();
|
||||||
return await session.update(data);
|
}
|
||||||
},
|
},
|
||||||
|
refresh: router.refresh,
|
||||||
logout: async () => {
|
logout: async () => {
|
||||||
await signOut();
|
await signOut();
|
||||||
posthog?.capture("logout");
|
posthog?.capture("logout");
|
||||||
|
@ -111,27 +127,7 @@ export const UserProvider = (props: { children?: React.ReactNode }) => {
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PreferencesProvider
|
{children}
|
||||||
initialValue={{
|
|
||||||
locale: user?.locale ?? undefined,
|
|
||||||
timeZone: user?.timeZone ?? undefined,
|
|
||||||
timeFormat: user?.timeFormat ?? undefined,
|
|
||||||
weekStart: user?.weekStart ?? undefined,
|
|
||||||
}}
|
|
||||||
onUpdate={async (newPreferences) => {
|
|
||||||
if (!isGuest) {
|
|
||||||
await updatePreferences.mutateAsync({
|
|
||||||
locale: newPreferences.locale ?? undefined,
|
|
||||||
timeZone: newPreferences.timeZone ?? undefined,
|
|
||||||
timeFormat: newPreferences.timeFormat ?? undefined,
|
|
||||||
weekStart: newPreferences.weekStart ?? undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await session.update(newPreferences);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</PreferencesProvider>
|
|
||||||
</UserContext.Provider>
|
</UserContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,16 +1,22 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import type { TimeFormat } from "@rallly/database";
|
import type { TimeFormat } from "@rallly/database";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useSetState } from "react-use";
|
import { useLocalStorage } from "react-use";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { useRequiredContext } from "@/components/use-required-context";
|
import { useRequiredContext } from "@/components/use-required-context";
|
||||||
|
import { useUser } from "@/components/user-provider";
|
||||||
|
import { trpc } from "@/trpc/client";
|
||||||
|
|
||||||
type Preferences = {
|
type Preferences = {
|
||||||
timeZone?: string | null;
|
timeZone?: string;
|
||||||
locale?: string | null;
|
timeFormat?: TimeFormat;
|
||||||
timeFormat?: TimeFormat | null;
|
weekStart?: number;
|
||||||
weekStart?: number | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const timeFormatSchema = z.enum(["hours12", "hours24"]);
|
||||||
|
|
||||||
type PreferencesContextValue = {
|
type PreferencesContextValue = {
|
||||||
preferences: Preferences;
|
preferences: Preferences;
|
||||||
updatePreferences: (preferences: Partial<Preferences>) => Promise<void>;
|
updatePreferences: (preferences: Partial<Preferences>) => Promise<void>;
|
||||||
|
@ -23,21 +29,58 @@ const PreferencesContext = React.createContext<PreferencesContextValue | null>(
|
||||||
export const PreferencesProvider = ({
|
export const PreferencesProvider = ({
|
||||||
children,
|
children,
|
||||||
initialValue,
|
initialValue,
|
||||||
onUpdate,
|
|
||||||
}: {
|
}: {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
initialValue: Partial<Preferences>;
|
initialValue: Partial<Preferences>;
|
||||||
onUpdate?: (preferences: Partial<Preferences>) => Promise<void>;
|
|
||||||
}) => {
|
}) => {
|
||||||
const [preferences, setPreferences] = useSetState<Preferences>(initialValue);
|
const { user } = useUser();
|
||||||
|
const [preferredTimezone, setPreferredTimezone] = useLocalStorage(
|
||||||
|
"rallly.preferredTimezone",
|
||||||
|
initialValue.timeZone,
|
||||||
|
);
|
||||||
|
const [preferredTimeFormat, setPreferredTimeFormat] = useLocalStorage(
|
||||||
|
"rallly.preferredTimeFormat",
|
||||||
|
initialValue.timeFormat,
|
||||||
|
{
|
||||||
|
raw: false,
|
||||||
|
serializer: timeFormatSchema.parse,
|
||||||
|
deserializer: timeFormatSchema.optional().catch(undefined).parse,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const [preferredWeekStart, setPreferredWeekStart] = useLocalStorage(
|
||||||
|
"rallly.preferredWeekStart",
|
||||||
|
initialValue.weekStart,
|
||||||
|
);
|
||||||
|
const updatePreferences = trpc.user.updatePreferences.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PreferencesContext.Provider
|
<PreferencesContext.Provider
|
||||||
value={{
|
value={{
|
||||||
preferences,
|
preferences: {
|
||||||
|
timeZone: preferredTimezone,
|
||||||
|
timeFormat: preferredTimeFormat,
|
||||||
|
weekStart: preferredWeekStart,
|
||||||
|
},
|
||||||
updatePreferences: async (newPreferences) => {
|
updatePreferences: async (newPreferences) => {
|
||||||
setPreferences(newPreferences);
|
if (newPreferences.timeZone) {
|
||||||
await onUpdate?.(newPreferences);
|
setPreferredTimezone(newPreferences.timeZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPreferences.timeFormat) {
|
||||||
|
setPreferredTimeFormat(newPreferences.timeFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPreferences.weekStart) {
|
||||||
|
setPreferredWeekStart(newPreferences.weekStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.isGuest) {
|
||||||
|
await updatePreferences.mutateAsync({
|
||||||
|
timeZone: newPreferences.timeZone ?? undefined,
|
||||||
|
timeFormat: newPreferences.timeFormat ?? undefined,
|
||||||
|
weekStart: newPreferences.weekStart ?? undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -18,6 +18,8 @@ export const getUser = cache(async () => {
|
||||||
image: true,
|
image: true,
|
||||||
locale: true,
|
locale: true,
|
||||||
timeZone: true,
|
timeZone: true,
|
||||||
|
timeFormat: true,
|
||||||
|
weekStart: true,
|
||||||
subscription: {
|
subscription: {
|
||||||
select: {
|
select: {
|
||||||
active: true,
|
active: true,
|
||||||
|
@ -37,6 +39,8 @@ export const getUser = cache(async () => {
|
||||||
image: user.image ?? undefined,
|
image: user.image ?? undefined,
|
||||||
locale: user.locale ?? undefined,
|
locale: user.locale ?? undefined,
|
||||||
timeZone: user.timeZone ?? undefined,
|
timeZone: user.timeZone ?? undefined,
|
||||||
|
timeFormat: user.timeFormat ?? undefined,
|
||||||
|
weekStart: user.weekStart ?? undefined,
|
||||||
isPro: user.subscription?.active ?? false,
|
isPro: user.subscription?.active ?? false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { redirect } from "next/navigation";
|
||||||
import NextAuth from "next-auth";
|
import NextAuth from "next-auth";
|
||||||
import type { Provider } from "next-auth/providers";
|
import type { Provider } from "next-auth/providers";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
import { CustomPrismaAdapter } from "./auth/adapters/prisma";
|
import { CustomPrismaAdapter } from "./auth/adapters/prisma";
|
||||||
import { isEmailBanned, isEmailBlocked } from "./auth/helpers/is-email-blocked";
|
import { isEmailBanned, isEmailBlocked } from "./auth/helpers/is-email-blocked";
|
||||||
|
@ -17,13 +16,6 @@ import { OIDCProvider } from "./auth/providers/oidc";
|
||||||
import { RegistrationTokenProvider } from "./auth/providers/registration-token";
|
import { RegistrationTokenProvider } from "./auth/providers/registration-token";
|
||||||
import { nextAuthConfig } from "./next-auth.config";
|
import { nextAuthConfig } from "./next-auth.config";
|
||||||
|
|
||||||
const sessionUpdateSchema = z.object({
|
|
||||||
locale: z.string().nullish(),
|
|
||||||
timeFormat: z.enum(["hours12", "hours24"]).nullish(),
|
|
||||||
timeZone: z.string().nullish(),
|
|
||||||
weekStart: z.number().nullish(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
auth: originalAuth,
|
auth: originalAuth,
|
||||||
handlers,
|
handlers,
|
||||||
|
@ -162,17 +154,7 @@ const {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
async jwt({ token, session }) {
|
async jwt({ token }) {
|
||||||
if (session) {
|
|
||||||
const parsed = sessionUpdateSchema.safeParse(session);
|
|
||||||
if (parsed.success) {
|
|
||||||
Object.entries(parsed.data).forEach(([key, value]) => {
|
|
||||||
token[key] = value;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error(parsed.error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const userId = token.sub;
|
const userId = token.sub;
|
||||||
const isGuest = userId?.startsWith("guest-");
|
const isGuest = userId?.startsWith("guest-");
|
||||||
|
|
||||||
|
@ -184,9 +166,6 @@ const {
|
||||||
select: {
|
select: {
|
||||||
name: true,
|
name: true,
|
||||||
email: true,
|
email: true,
|
||||||
timeFormat: true,
|
|
||||||
timeZone: true,
|
|
||||||
weekStart: true,
|
|
||||||
image: true,
|
image: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -195,10 +174,6 @@ const {
|
||||||
token.name = user.name;
|
token.name = user.name;
|
||||||
token.email = user.email;
|
token.email = user.email;
|
||||||
token.picture = user.image;
|
token.picture = user.image;
|
||||||
token.timeFormat = user.timeFormat;
|
|
||||||
token.timeZone = user.timeZone;
|
|
||||||
token.weekStart = user.weekStart;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -97,6 +97,8 @@ export const auth = router({
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
code: z.string(),
|
code: z.string(),
|
||||||
timeZone: z.string().optional(),
|
timeZone: z.string().optional(),
|
||||||
|
weekStart: z.number().min(0).max(6).optional(),
|
||||||
|
timeFormat: z.enum(["hours12", "hours24"]).optional(),
|
||||||
locale: z.string().optional(),
|
locale: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
@ -118,6 +120,8 @@ export const auth = router({
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
timeZone: input.timeZone,
|
timeZone: input.timeZone,
|
||||||
|
timeFormat: input.timeFormat,
|
||||||
|
weekStart: input.weekStart,
|
||||||
locale: input.locale,
|
locale: input.locale,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -139,6 +143,9 @@ export const auth = router({
|
||||||
name: user.name,
|
name: user.name,
|
||||||
timeZone: input.timeZone,
|
timeZone: input.timeZone,
|
||||||
locale: input.locale,
|
locale: input.locale,
|
||||||
|
tier: "hobby",
|
||||||
|
weekStart: input.weekStart,
|
||||||
|
timeFormat: input.timeFormat,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,11 +14,11 @@ import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import timezone from "dayjs/plugin/timezone";
|
import timezone from "dayjs/plugin/timezone";
|
||||||
import updateLocale from "dayjs/plugin/updateLocale";
|
import updateLocale from "dayjs/plugin/updateLocale";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useAsync } from "react-use";
|
import { useAsync } from "react-use";
|
||||||
|
|
||||||
import { usePreferences } from "@/contexts/preferences";
|
import { usePreferences } from "@/contexts/preferences";
|
||||||
|
import { useTranslation } from "@/i18n/client";
|
||||||
import { getBrowserTimeZone, normalizeTimeZone } from "@/utils/date-time-utils";
|
import { getBrowserTimeZone, normalizeTimeZone } from "@/utils/date-time-utils";
|
||||||
|
|
||||||
import { useRequiredContext } from "../components/use-required-context";
|
import { useRequiredContext } from "../components/use-required-context";
|
||||||
|
@ -203,11 +203,10 @@ export const DayjsProvider: React.FunctionComponent<{
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}> = ({ config, children }) => {
|
}> = ({ config, children }) => {
|
||||||
const locale = useParams()?.locale as string;
|
const locale = config?.locale ?? "en";
|
||||||
const l = config?.locale ?? locale ?? "en";
|
|
||||||
const state = useAsync(async () => {
|
const state = useAsync(async () => {
|
||||||
return await dayjsLocales[l].import();
|
return await dayjsLocales[locale].import();
|
||||||
}, [l]);
|
}, [locale]);
|
||||||
|
|
||||||
const preferredTimeZone = React.useMemo(
|
const preferredTimeZone = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -234,7 +233,7 @@ export const DayjsProvider: React.FunctionComponent<{
|
||||||
}
|
}
|
||||||
|
|
||||||
const dayjsLocale = state.value;
|
const dayjsLocale = state.value;
|
||||||
const localeConfig = dayjsLocales[l];
|
const localeConfig = dayjsLocales[locale];
|
||||||
const localeTimeFormat = localeConfig.timeFormat;
|
const localeTimeFormat = localeConfig.timeFormat;
|
||||||
|
|
||||||
if (config?.localeOverrides) {
|
if (config?.localeOverrides) {
|
||||||
|
@ -278,10 +277,11 @@ export const ConnectedDayjsProvider = ({
|
||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren) => {
|
}: React.PropsWithChildren) => {
|
||||||
const { preferences } = usePreferences();
|
const { preferences } = usePreferences();
|
||||||
|
const { i18n } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<DayjsProvider
|
<DayjsProvider
|
||||||
config={{
|
config={{
|
||||||
locale: preferences.locale ?? undefined,
|
locale: i18n.language,
|
||||||
timeZone: preferences.timeZone ?? undefined,
|
timeZone: preferences.timeZone ?? undefined,
|
||||||
localeOverrides: {
|
localeOverrides: {
|
||||||
weekStart: preferences.weekStart ?? undefined,
|
weekStart: preferences.weekStart ?? undefined,
|
||||||
|
|
Loading…
Add table
Reference in a new issue