refactored how avatars are being handled

This commit is contained in:
daniel31x13 2023-10-28 00:45:14 -04:00
parent f9eedadb9f
commit cdcfabec0b
21 changed files with 55 additions and 85 deletions

View File

@ -68,7 +68,7 @@ export default function CollectionCard({ collection, className }: Props) {
return ( return (
<ProfilePhoto <ProfilePhoto
key={i} key={i}
src={`/api/v1/avatar/${e.userId}?${Date.now()}`} src={e.user.image ? e.user.image : undefined}
className="-mr-3 border-[3px]" className="-mr-3 border-[3px]"
/> />
); );

View File

@ -238,7 +238,7 @@ export default function TeamManagement({
)} )}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ProfilePhoto <ProfilePhoto
src={`/api/v1/avatar/${e.userId}?${Date.now()}`} src={e.user.image ? e.user.image : undefined}
className="border-[3px]" className="border-[3px]"
/> />
<div> <div>
@ -425,7 +425,7 @@ export default function TeamManagement({
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ProfilePhoto <ProfilePhoto
src={`/api/v1/avatar/${collection.ownerId}?${Date.now()}`} src={`uploads/avatar/${collection.ownerId}.jpg`}
className="border-[3px]" className="border-[3px]"
/> />
<div> <div>

View File

@ -83,7 +83,7 @@ export default function Navbar() {
id="profile-dropdown" id="profile-dropdown"
> >
<ProfilePhoto <ProfilePhoto
src={account.profilePic} src={account.image ? account.image : undefined}
priority={true} priority={true}
className="sm:group-hover:h-8 sm:group-hover:w-8 duration-100 border-[3px]" className="sm:group-hover:h-8 sm:group-hover:w-8 duration-100 border-[3px]"
/> />

View File

@ -2,38 +2,28 @@ import React, { useEffect, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faUser } from "@fortawesome/free-solid-svg-icons"; import { faUser } from "@fortawesome/free-solid-svg-icons";
import Image from "next/image"; import Image from "next/image";
import avatarExists from "@/lib/client/avatarExists";
type Props = { type Props = {
src: string; src?: string;
className?: string; className?: string;
emptyImage?: boolean; emptyImage?: boolean;
status?: Function;
priority?: boolean; priority?: boolean;
}; };
export default function ProfilePhoto({ export default function ProfilePhoto({ src, className, priority }: Props) {
src, const [image, setImage] = useState("");
className,
emptyImage,
status,
priority,
}: Props) {
const [error, setError] = useState<boolean>(emptyImage || true);
const checkAvatarExistence = async () => {
const canPass = await avatarExists(src);
setError(!canPass);
};
useEffect(() => { useEffect(() => {
if (src) checkAvatarExistence(); console.log(src);
if (src && !src?.includes("base64"))
setImage(`/api/v1/${src.replace("uploads/", "").replace(".jpg", "")}`);
else if (!src) setImage("");
else {
setImage(src);
}
}, [src]);
status && status(error || !src); return !image ? (
}, [src, error]);
return error || !src ? (
<div <div
className={`bg-sky-600 dark:bg-sky-600 text-white h-10 w-10 aspect-square shadow rounded-full border border-slate-200 dark:border-neutral-700 flex items-center justify-center ${className}`} className={`bg-sky-600 dark:bg-sky-600 text-white h-10 w-10 aspect-square shadow rounded-full border border-slate-200 dark:border-neutral-700 flex items-center justify-center ${className}`}
> >
@ -42,7 +32,7 @@ export default function ProfilePhoto({
) : ( ) : (
<Image <Image
alt="" alt=""
src={src} src={image}
height={112} height={112}
width={112} width={112}
priority={priority} priority={priority}

View File

@ -18,6 +18,7 @@ export default async function getCollection(userId: number) {
select: { select: {
username: true, username: true,
name: true, name: true,
image: true,
}, },
}, },
}, },

View File

@ -42,6 +42,7 @@ export default async function getPublicUserById(
const data = { const data = {
name: lessSensitiveInfo.name, name: lessSensitiveInfo.name,
username: lessSensitiveInfo.username, username: lessSensitiveInfo.username,
image: lessSensitiveInfo.image,
}; };
return { response: data, status: 200 }; return { response: data, status: 200 };

View File

@ -89,12 +89,10 @@ export default async function updateUserById(
// Avatar Settings // Avatar Settings
const profilePic = data.profilePic; if (data.image?.startsWith("data:image/jpeg;base64")) {
if (data.image.length < 1572864) {
if (profilePic.startsWith("data:image/jpeg;base64")) {
if (data.profilePic.length < 1572864) {
try { try {
const base64Data = profilePic.replace(/^data:image\/jpeg;base64,/, ""); const base64Data = data.image.replace(/^data:image\/jpeg;base64,/, "");
createFolder({ filePath: `uploads/avatar` }); createFolder({ filePath: `uploads/avatar` });
@ -113,7 +111,7 @@ export default async function updateUserById(
status: 400, status: 400,
}; };
} }
} else if (profilePic == "") { } else if (data.image == "") {
removeFile({ filePath: `uploads/avatar/${sessionUser.id}.jpg` }); removeFile({ filePath: `uploads/avatar/${sessionUser.id}.jpg` });
} }
@ -131,6 +129,7 @@ export default async function updateUserById(
username: data.username.toLowerCase().trim(), username: data.username.toLowerCase().trim(),
email: data.email?.toLowerCase().trim(), email: data.email?.toLowerCase().trim(),
isPrivate: data.isPrivate, isPrivate: data.isPrivate,
image: data.image ? `uploads/avatar/${sessionUser.id}.jpg` : "",
archiveAsScreenshot: data.archiveAsScreenshot, archiveAsScreenshot: data.archiveAsScreenshot,
archiveAsPDF: data.archiveAsPDF, archiveAsPDF: data.archiveAsPDF,
archiveAsWaybackMachine: data.archiveAsWaybackMachine, archiveAsWaybackMachine: data.archiveAsWaybackMachine,
@ -197,7 +196,7 @@ export default async function updateUserById(
const response: Omit<AccountSettings, "password"> = { const response: Omit<AccountSettings, "password"> = {
...userInfo, ...userInfo,
whitelistedUsers: newWhitelistedUsernames, whitelistedUsers: newWhitelistedUsernames,
profilePic: `/api/v1/avatar/${userInfo.id}?${Date.now()}`, image: userInfo.image ? `${userInfo.image}?${Date.now()}` : "",
}; };
return { response, status: 200 }; return { response, status: 200 };

View File

@ -70,7 +70,7 @@ async function migrateToV2() {
if (res) { if (res) {
await prisma.user.update({ await prisma.user.update({
where: { id: user.id }, where: { id: user.id },
data: { imagePath: path }, data: { image: path },
}); });
console.log(`Updated avatar for avatar ${user.id}`); console.log(`Updated avatar for avatar ${user.id}`);
} else { } else {

View File

@ -9,14 +9,12 @@ import s3Client from "./s3Client";
import util from "util"; import util from "util";
type ReturnContentTypes = type ReturnContentTypes =
| "text/html" | "text/plain"
| "image/jpeg" | "image/jpeg"
| "image/png" | "image/png"
| "application/pdf"; | "application/pdf";
export default async function readFile(filePath: string) { export default async function readFile(filePath: string) {
const isRequestingAvatar = filePath.startsWith("uploads/avatar");
let contentType: ReturnContentTypes; let contentType: ReturnContentTypes;
if (s3Client) { if (s3Client) {
@ -41,12 +39,12 @@ export default async function readFile(filePath: string) {
try { try {
await headObjectAsync(bucketParams); await headObjectAsync(bucketParams);
} catch (err) { } catch (err) {
contentType = "text/html"; contentType = "text/plain";
returnObject = { returnObject = {
file: isRequestingAvatar ? "File not found." : fileNotFoundTemplate, file: "File not found.",
contentType, contentType,
status: isRequestingAvatar ? 200 : 400, status: 400,
}; };
} }
@ -71,9 +69,9 @@ export default async function readFile(filePath: string) {
} catch (err) { } catch (err) {
console.log("Error:", err); console.log("Error:", err);
contentType = "text/html"; contentType = "text/plain";
return { return {
file: "An internal occurred, please contact support.", file: "An internal occurred, please contact the support team.",
contentType, contentType,
}; };
} }
@ -92,9 +90,9 @@ export default async function readFile(filePath: string) {
if (!fs.existsSync(creationPath)) if (!fs.existsSync(creationPath))
return { return {
file: isRequestingAvatar ? "File not found." : fileNotFoundTemplate, file: "File not found.",
contentType: "text/html", contentType: "text/plain",
status: isRequestingAvatar ? 200 : 400, status: 400,
}; };
else { else {
const file = fs.readFileSync(creationPath); const file = fs.readFileSync(creationPath);

View File

@ -35,6 +35,7 @@ const addMemberToCollection = async (
id: user.id, id: user.id,
name: user.name, name: user.name,
username: user.username, username: user.username,
image: user.image,
}, },
}); });
} }

View File

@ -1,13 +0,0 @@
const avatarCache = new Map();
export default async function avatarExists(fileUrl: string): Promise<boolean> {
if (avatarCache.has(fileUrl)) {
return avatarCache.get(fileUrl);
}
const response = await fetch(fileUrl, { method: "HEAD" });
const exists = !(response.headers.get("content-type") === "text/html");
avatarCache.set(fileUrl, exists);
return exists;
}

View File

@ -5,6 +5,8 @@ export default async function getPublicUserData(id: number | string) {
const data = await response.json(); const data = await response.json();
console.log(data);
if (!response.ok) toast.error(data.response); if (!response.ok) toast.error(data.response);
return data.response; return data.response;

View File

@ -3,6 +3,7 @@ const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
images: { images: {
domains: ["t2.gstatic.com"], domains: ["t2.gstatic.com"],
minimumCacheTTL: 10,
}, },
}; };

View File

@ -13,7 +13,7 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
if (!userId || !username) if (!userId || !username)
return res return res
.setHeader("Content-Type", "text/html") .setHeader("Content-Type", "text/plain")
.status(401) .status(401)
.send("You must be logged in."); .send("You must be logged in.");
else if (session?.user?.isSubscriber === false) else if (session?.user?.isSubscriber === false)
@ -24,7 +24,7 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
if (!queryId) if (!queryId)
return res return res
.setHeader("Content-Type", "text/html") .setHeader("Content-Type", "text/plain")
.status(401) .status(401)
.send("Invalid parameters."); .send("Invalid parameters.");
@ -44,8 +44,9 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
if (targetUser?.isPrivate && !whitelistedUsernames?.includes(username)) { if (targetUser?.isPrivate && !whitelistedUsernames?.includes(username)) {
return res return res
.setHeader("Content-Type", "text/html") .setHeader("Content-Type", "text/plain")
.send("This profile is private."); .status(400)
.send("File not found.");
} }
} }

View File

@ -104,7 +104,7 @@ export default function Index() {
return ( return (
<ProfilePhoto <ProfilePhoto
key={i} key={i}
src={`/api/v1/avatar/${e.userId}?${Date.now()}`} src={e.user.image ? e.user.image : undefined}
className="-mr-3 border-[3px]" className="-mr-3 border-[3px]"
/> />
); );

View File

@ -15,11 +15,8 @@ import useModalStore from "@/store/modals";
import SortDropdown from "@/components/SortDropdown"; import SortDropdown from "@/components/SortDropdown";
import { Sort } from "@/types/global"; import { Sort } from "@/types/global";
import useSort from "@/hooks/useSort"; import useSort from "@/hooks/useSort";
import { useTheme } from "next-themes";
export default function Collections() { export default function Collections() {
const { theme } = useTheme();
const { collections } = useCollectionStore(); const { collections } = useCollectionStore();
const [expandDropdown, setExpandDropdown] = useState(false); const [expandDropdown, setExpandDropdown] = useState(false);
const [sortDropdown, setSortDropdown] = useState(false); const [sortDropdown, setSortDropdown] = useState(false);

View File

@ -21,13 +21,8 @@ export default function Account() {
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER; const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
const [profileStatus, setProfileStatus] = useState(true);
const [submitLoader, setSubmitLoader] = useState(false); const [submitLoader, setSubmitLoader] = useState(false);
const handleProfileStatus = (e: boolean) => {
setProfileStatus(!e);
};
const { account, updateAccount } = useAccountStore(); const { account, updateAccount } = useAccountStore();
const [user, setUser] = useState<AccountSettings>( const [user, setUser] = useState<AccountSettings>(
@ -40,12 +35,11 @@ export default function Account() {
username: "", username: "",
email: "", email: "",
emailVerified: null, emailVerified: null,
image: null, image: "",
isPrivate: true, isPrivate: true,
// @ts-ignore // @ts-ignore
createdAt: null, createdAt: null,
whitelistedUsers: [], whitelistedUsers: [],
profilePic: "",
} as unknown as AccountSettings) } as unknown as AccountSettings)
); );
@ -54,6 +48,7 @@ export default function Account() {
} }
useEffect(() => { useEffect(() => {
console.log(account);
if (!objectIsEmpty(account)) setUser({ ...account }); if (!objectIsEmpty(account)) setUser({ ...account });
}, [account]); }, [account]);
@ -68,7 +63,7 @@ export default function Account() {
) { ) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
setUser({ ...user, profilePic: reader.result as string }); setUser({ ...user, image: reader.result as string });
}; };
reader.readAsDataURL(resizedFile); reader.readAsDataURL(resizedFile);
} else { } else {
@ -220,16 +215,15 @@ export default function Account() {
<div className="w-28 h-28 flex items-center justify-center rounded-full relative"> <div className="w-28 h-28 flex items-center justify-center rounded-full relative">
<ProfilePhoto <ProfilePhoto
priority={true} priority={true}
src={user.profilePic} src={user.image ? user.image : undefined}
className="h-auto border-none w-28" className="h-auto border-none w-28"
status={handleProfileStatus}
/> />
{profileStatus && ( {user.image && (
<div <div
onClick={() => onClick={() =>
setUser({ setUser({
...user, ...user,
profilePic: "", image: "",
}) })
} }
className="absolute top-1 left-1 w-5 h-5 flex items-center justify-center border p-1 border-slate-200 dark:border-neutral-700 rounded-full bg-white dark:bg-neutral-800 text-center select-none cursor-pointer duration-100 hover:text-red-500" className="absolute top-1 left-1 w-5 h-5 flex items-center justify-center border p-1 border-slate-200 dark:border-neutral-700 rounded-full bg-white dark:bg-neutral-800 text-center select-none cursor-pointer duration-100 hover:text-red-500"

View File

@ -0,0 +1 @@
ALTER TABLE "User" RENAME COLUMN "imagePath" TO "image";

View File

@ -15,7 +15,7 @@ model User {
email String? @unique email String? @unique
emailVerified DateTime? emailVerified DateTime?
imagePath String? image String?
password String password String
collections Collection[] collections Collection[]

View File

@ -19,9 +19,7 @@ const useAccountStore = create<AccountStore>()((set) => ({
const data = await response.json(); const data = await response.json();
const profilePic = `/api/v1/avatar/${data.response.id}?${Date.now()}`; if (response.ok) set({ account: { ...data.response } });
if (response.ok) set({ account: { ...data.response, profilePic } });
}, },
updateAccount: async (user) => { updateAccount: async (user) => {
const response = await fetch(`/api/v1/users/${user.id}`, { const response = await fetch(`/api/v1/users/${user.id}`, {

View File

@ -35,7 +35,6 @@ export interface CollectionIncludingMembersAndLinkCount
} }
export interface AccountSettings extends User { export interface AccountSettings extends User {
profilePic: string;
newPassword?: string; newPassword?: string;
whitelistedUsers: string[]; whitelistedUsers: string[];
} }
@ -79,7 +78,7 @@ interface CollectionIncludingLinks extends Collection {
links: LinksIncludingTags[]; links: LinksIncludingTags[];
} }
export interface Backup extends Omit<User, "password" | "id" | "image"> { export interface Backup extends Omit<User, "password" | "id"> {
collections: CollectionIncludingLinks[]; collections: CollectionIncludingLinks[];
} }