refactored how avatars are being handled
This commit is contained in:
parent
f9eedadb9f
commit
cdcfabec0b
|
@ -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]"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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]"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -18,6 +18,7 @@ export default async function getCollection(userId: number) {
|
||||||
select: {
|
select: {
|
||||||
username: true,
|
username: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
image: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
|
|
@ -3,6 +3,7 @@ const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
images: {
|
images: {
|
||||||
domains: ["t2.gstatic.com"],
|
domains: ["t2.gstatic.com"],
|
||||||
|
minimumCacheTTL: 10,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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]"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE "User" RENAME COLUMN "imagePath" TO "image";
|
|
@ -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[]
|
||||||
|
|
|
@ -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}`, {
|
||||||
|
|
|
@ -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[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Ŝarĝante…
Reference in New Issue