Use profile image from oauth provider (#1330)

This commit is contained in:
Luke Vella 2024-09-07 16:57:07 +01:00 committed by GitHub
parent be216344e2
commit 364bb8b83f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 95 additions and 29 deletions

View file

@ -11,13 +11,11 @@ declare module "next-auth" {
interface Session { interface Session {
user: { user: {
id: string; id: string;
name?: string | null;
email?: string | null;
timeZone?: string | null; timeZone?: string | null;
timeFormat?: TimeFormat | null; timeFormat?: TimeFormat | null;
locale?: string | null; locale?: string | null;
weekStart?: number | null; weekStart?: number | null;
}; } & DefaultSession["user"];
} }
interface User extends DefaultUser { interface User extends DefaultUser {

View file

@ -18,9 +18,9 @@ import {
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { CurrentUserAvatar } from "@/components/current-user-avatar";
import { ProBadge } from "@/components/pro-badge"; import { ProBadge } from "@/components/pro-badge";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { CurrentUserAvatar } from "@/components/user";
import { IfGuest, useUser } from "@/components/user-provider"; import { IfGuest, useUser } from "@/components/user-provider";
import { IfFreeUser } from "@/contexts/plan"; import { IfFreeUser } from "@/contexts/plan";
import { IconComponent } from "@/types"; import { IconComponent } from "@/types";

View file

@ -0,0 +1,14 @@
"use client";
import { Avatar, AvatarFallback, AvatarImage } from "@rallly/ui/avatar";
import { useUser } from "@/components/user-provider";
export const CurrentUserAvatar = ({ className }: { className?: string }) => {
const { user } = useUser();
return (
<Avatar className={className}>
<AvatarImage src={user.image ?? undefined} />
<AvatarFallback>{user.name[0]}</AvatarFallback>
</Avatar>
);
};

View file

@ -9,8 +9,8 @@ import {
import { Input } from "@rallly/ui/input"; import { Input } from "@rallly/ui/input";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { CurrentUserAvatar } from "@/components/current-user-avatar";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { UserAvatar } from "@/components/user";
import { useUser } from "@/components/user-provider"; import { useUser } from "@/components/user-provider";
export const ProfileSettings = () => { export const ProfileSettings = () => {
@ -26,9 +26,7 @@ export const ProfileSettings = () => {
}, },
}); });
const { control, watch, handleSubmit, formState, reset } = form; const { control, handleSubmit, formState, reset } = form;
const watchName = watch("name");
return ( return (
<div className="grid gap-y-4"> <div className="grid gap-y-4">
@ -41,7 +39,7 @@ export const ProfileSettings = () => {
> >
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
<div> <div>
<UserAvatar name={watchName} size="lg" /> <CurrentUserAvatar className="size-14" />
</div> </div>
<FormField <FormField
control={control} control={control}

View file

@ -26,10 +26,10 @@ import {
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { CurrentUserAvatar } from "@/components/current-user-avatar";
import { LoginLink } from "@/components/login-link"; import { LoginLink } from "@/components/login-link";
import { RegisterLink } from "@/components/register-link"; import { RegisterLink } from "@/components/register-link";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { CurrentUserAvatar } from "@/components/user";
import { IfCloudHosted, IfSelfHosted } from "@/contexts/environment"; import { IfCloudHosted, IfSelfHosted } from "@/contexts/environment";
import { Plan, usePlan } from "@/contexts/plan"; import { Plan, usePlan } from "@/contexts/plan";
import { isFeedbackEnabled } from "@/utils/constants"; import { isFeedbackEnabled } from "@/utils/constants";
@ -57,7 +57,7 @@ export const UserDropdown = ({ className }: { className?: string }) => {
className={cn("group min-w-0", className)} className={cn("group min-w-0", className)}
> >
<Button variant="ghost"> <Button variant="ghost">
<CurrentUserAvatar size="xs" className="shrink-0 " /> <CurrentUserAvatar className="size-6" />
<span className="truncate">{user.name}</span> <span className="truncate">{user.name}</span>
<Icon> <Icon>
<ChevronDownIcon /> <ChevronDownIcon />

View file

@ -21,6 +21,7 @@ const userSchema = z.object({
timeZone: z.string().nullish(), timeZone: z.string().nullish(),
timeFormat: z.enum(["hours12", "hours24"]).nullish(), timeFormat: z.enum(["hours12", "hours24"]).nullish(),
weekStart: z.number().min(0).max(6).nullish(), weekStart: z.number().min(0).max(6).nullish(),
image: z.string().nullish(),
}); });
export const UserContext = React.createContext<{ export const UserContext = React.createContext<{
@ -76,9 +77,10 @@ export const UserProvider = (props: { children?: React.ReactNode }) => {
id: user.id as string, id: user.id as string,
name: user.name ?? t("guest"), name: user.name ?? t("guest"),
email: user.email || null, email: user.email || null,
isGuest: !user.email, isGuest,
tier, tier,
timeZone: user.timeZone ?? null, timeZone: user.timeZone ?? null,
image: user.image ?? null,
}, },
refresh: session.update, refresh: session.update,
ownsObject: ({ userId }) => { ownsObject: ({ userId }) => {

View file

@ -3,24 +3,8 @@ import { cn } from "@rallly/ui";
import { Icon } from "@rallly/ui/icon"; import { Icon } from "@rallly/ui/icon";
import { User2Icon } from "lucide-react"; import { User2Icon } from "lucide-react";
import { useUser } from "@/components/user-provider";
import { getRandomAvatarColor } from "@/utils/color-hash"; import { getRandomAvatarColor } from "@/utils/color-hash";
export const CurrentUserAvatar = ({
size = "md",
className,
}: Omit<UserAvatarProps, "name">) => {
const { user } = useUser();
return (
<UserAvatar
className={className}
name={user.isGuest ? undefined : user.name}
size={size}
/>
);
};
interface UserAvatarProps { interface UserAvatarProps {
name?: string; name?: string;
size?: "xs" | "sm" | "md" | "lg"; size?: "xs" | "sm" | "md" | "lg";

View file

@ -54,6 +54,7 @@ const providers: Provider[] = [
locale: true, locale: true,
timeFormat: true, timeFormat: true,
timeZone: true, timeZone: true,
image: true,
}, },
}); });
@ -265,6 +266,7 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
timeZone: session.timeZone, timeZone: session.timeZone,
weekStart: session.weekStart, weekStart: session.weekStart,
name: session.name, name: session.name,
image: session.image,
}, },
}); });
} catch (e) { } catch (e) {
@ -278,6 +280,7 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
token.timeFormat = user.timeFormat; token.timeFormat = user.timeFormat;
token.timeZone = user.timeZone; token.timeZone = user.timeZone;
token.weekStart = user.weekStart; token.weekStart = user.weekStart;
token.picture = user.image;
} }
return token; return token;
}, },
@ -288,6 +291,7 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
session.user.timeZone = token.timeZone; session.user.timeZone = token.timeZone;
session.user.locale = token.locale; session.user.locale = token.locale;
session.user.weekStart = token.weekStart; session.user.weekStart = token.weekStart;
session.user.image = token.picture;
return session; return session;
}, },
}, },

View file

@ -13,6 +13,7 @@
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.4", "@radix-ui/react-dropdown-menu": "^2.0.4",

View file

@ -0,0 +1,50 @@
"use client";
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@rallly/ui";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex size-10 shrink-0 overflow-hidden rounded-full",
className,
)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"bg-muted flex h-full w-full items-center justify-center rounded-full",
className,
)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };

View file

@ -7,7 +7,7 @@ import * as React from "react";
import { Dialog, DialogContent } from "./dialog"; import { Dialog, DialogContent } from "./dialog";
import { cn } from "./lib/utils"; import { cn } from "./lib/utils";
import { Icon } from "@rallly/ui/icon"; import { Icon } from "./icon";
const Command = React.forwardRef< const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>, React.ElementRef<typeof CommandPrimitive>,

View file

@ -2,6 +2,11 @@
"extends": "@rallly/tsconfig/next.json", "extends": "@rallly/tsconfig/next.json",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": {
"@/utils": ["src/lib/utils.ts"],
"@/components/*": ["src/*"],
"@/ui/*": ["src/*"],
},
}, },
"include": ["**/*.ts", "**/*.tsx"], "include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"], "exclude": ["node_modules"],

View file

@ -2872,6 +2872,16 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-avatar@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.1.0.tgz#457c81334c93f4608df15f081e7baa286558d6a2"
integrity sha512-Q/PbuSMk/vyAd/UoIShVGZ7StHHeRFYU7wXmi5GV+8cLXflZAEpHL/F697H1klrzxKXNtZ97vWiC0q3RKUH8UA==
dependencies:
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-checkbox@^1.0.4": "@radix-ui/react-checkbox@^1.0.4":
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz#98f22c38d5010dd6df4c5744cac74087e3275f4b" resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz#98f22c38d5010dd6df4c5744cac74087e3275f4b"