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 (
<ProfilePhoto
key={i}
src={`/api/v1/avatar/${e.userId}?${Date.now()}`}
src={e.user.image ? e.user.image : undefined}
className="-mr-3 border-[3px]"
/>
);

View File

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

View File

@ -83,7 +83,7 @@ export default function Navbar() {
id="profile-dropdown"
>
<ProfilePhoto
src={account.profilePic}
src={account.image ? account.image : undefined}
priority={true}
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 { faUser } from "@fortawesome/free-solid-svg-icons";
import Image from "next/image";
import avatarExists from "@/lib/client/avatarExists";
type Props = {
src: string;
src?: string;
className?: string;
emptyImage?: boolean;
status?: Function;
priority?: boolean;
};
export default function ProfilePhoto({
src,
className,
emptyImage,
status,
priority,
}: Props) {
const [error, setError] = useState<boolean>(emptyImage || true);
const checkAvatarExistence = async () => {
const canPass = await avatarExists(src);
setError(!canPass);
};
export default function ProfilePhoto({ src, className, priority }: Props) {
const [image, setImage] = useState("");
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);
}, [src, error]);
return error || !src ? (
return !image ? (
<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}`}
>
@ -42,7 +32,7 @@ export default function ProfilePhoto({
) : (
<Image
alt=""
src={src}
src={image}
height={112}
width={112}
priority={priority}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,6 +35,7 @@ const addMemberToCollection = async (
id: user.id,
name: user.name,
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();
console.log(data);
if (!response.ok) toast.error(data.response);
return data.response;

View File

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

View File

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

View File

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

View File

@ -15,11 +15,8 @@ import useModalStore from "@/store/modals";
import SortDropdown from "@/components/SortDropdown";
import { Sort } from "@/types/global";
import useSort from "@/hooks/useSort";
import { useTheme } from "next-themes";
export default function Collections() {
const { theme } = useTheme();
const { collections } = useCollectionStore();
const [expandDropdown, setExpandDropdown] = 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 [profileStatus, setProfileStatus] = useState(true);
const [submitLoader, setSubmitLoader] = useState(false);
const handleProfileStatus = (e: boolean) => {
setProfileStatus(!e);
};
const { account, updateAccount } = useAccountStore();
const [user, setUser] = useState<AccountSettings>(
@ -40,12 +35,11 @@ export default function Account() {
username: "",
email: "",
emailVerified: null,
image: null,
image: "",
isPrivate: true,
// @ts-ignore
createdAt: null,
whitelistedUsers: [],
profilePic: "",
} as unknown as AccountSettings)
);
@ -54,6 +48,7 @@ export default function Account() {
}
useEffect(() => {
console.log(account);
if (!objectIsEmpty(account)) setUser({ ...account });
}, [account]);
@ -68,7 +63,7 @@ export default function Account() {
) {
const reader = new FileReader();
reader.onload = () => {
setUser({ ...user, profilePic: reader.result as string });
setUser({ ...user, image: reader.result as string });
};
reader.readAsDataURL(resizedFile);
} else {
@ -220,16 +215,15 @@ export default function Account() {
<div className="w-28 h-28 flex items-center justify-center rounded-full relative">
<ProfilePhoto
priority={true}
src={user.profilePic}
src={user.image ? user.image : undefined}
className="h-auto border-none w-28"
status={handleProfileStatus}
/>
{profileStatus && (
{user.image && (
<div
onClick={() =>
setUser({
...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"

View File

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

View File

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

View File

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

View File

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