mirror of
https://github.com/fergalmoran/mixyboos.git
synced 2025-12-22 09:41:39 +00:00
Image editing infra added
This commit is contained in:
@@ -11,7 +11,11 @@ const config = {
|
||||
esmExternals: false,
|
||||
},
|
||||
images: {
|
||||
domains: ["cloudflare-ipfs.com", "avatars.githubusercontent.com"],
|
||||
domains: [
|
||||
"cloudflare-ipfs.com",
|
||||
"avatars.githubusercontent.com",
|
||||
"uploadthing.com",
|
||||
],
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
|
||||
768
package-lock.json
generated
768
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
56
package.json
56
package.json
@@ -13,21 +13,22 @@
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.14",
|
||||
"@next-auth/prisma-adapter": "^1.0.6",
|
||||
"@headlessui/react": "^1.7.15",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^4.15.0",
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"@t3-oss/env-nextjs": "^0.4.0",
|
||||
"@tanstack/react-query": "^4.29.12",
|
||||
"@t3-oss/env-nextjs": "^0.4.1",
|
||||
"@tanstack/react-query": "^4.29.14",
|
||||
"@trpc/client": "^10.28.1",
|
||||
"@trpc/next": "^10.28.1",
|
||||
"@trpc/next": "^10.30.0",
|
||||
"@trpc/react-query": "^10.28.1",
|
||||
"@trpc/server": "^10.28.1",
|
||||
"@uploadthing/react": "^4.1.3",
|
||||
"argon2": "^0.30.3",
|
||||
"classnames": "^2.3.2",
|
||||
"http-status-codes": "^2.2.0",
|
||||
"install": "^0.13.0",
|
||||
"next": "13.4.5",
|
||||
"next": "13.4.6",
|
||||
"next-auth": "^4.22.1",
|
||||
"quirrel": "^1.13.4",
|
||||
"react": "18.2.0",
|
||||
@@ -35,54 +36,55 @@
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^4.9.0",
|
||||
"superjson": "1.12.3",
|
||||
"uploadthing": "^4.1.3",
|
||||
"yup": "^1.2.0",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.0.1",
|
||||
"@hookform/resolvers": "^3.1.1",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.0.2",
|
||||
"@radix-ui/react-avatar": "^1.0.3",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@types/eslint": "^8.40.0",
|
||||
"@types/node": "^20.2.5",
|
||||
"@types/eslint": "^8.40.2",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/prettier": "^2.7.3",
|
||||
"@types/react": "^18.2.7",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@types/react": "^18.2.12",
|
||||
"@types/react-dom": "^18.2.5",
|
||||
"@types/superagent": "^4.1.18",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
||||
"@typescript-eslint/parser": "^5.59.8",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.11",
|
||||
"@typescript-eslint/parser": "^5.59.11",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"clsx": "^1.2.1",
|
||||
"concurrently": "^8.0.1",
|
||||
"dayjs": "^1.11.7",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-next": "^13.4.4",
|
||||
"hls.js": "^1.4.4",
|
||||
"lucide-react": "^0.229.0",
|
||||
"concurrently": "^8.2.0",
|
||||
"dayjs": "^1.11.8",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-next": "^13.4.6",
|
||||
"hls.js": "^1.4.6",
|
||||
"lucide-react": "^0.244.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "^8.4.24",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||
"prisma": "^4.15.0",
|
||||
"pusher": "^5.1.3",
|
||||
"pusher-js": "^8.0.2",
|
||||
"pusher-js": "^8.1.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.44.2",
|
||||
"recharts": "^2.6.2",
|
||||
"react-hook-form": "^7.44.3",
|
||||
"recharts": "^2.7.1",
|
||||
"retry": "^0.13.1",
|
||||
"superagent": "^8.0.9",
|
||||
"tailwind-merge": "^1.12.0",
|
||||
"tailwind-merge": "^1.13.2",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"typescript": "^5.0.4",
|
||||
"tailwindcss-animate": "^1.0.6",
|
||||
"typescript": "^5.1.3",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
|
||||
@@ -95,9 +95,9 @@ model User {
|
||||
password String?
|
||||
name String?
|
||||
bio String?
|
||||
email String? @unique
|
||||
email String @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
profileImage String?
|
||||
headerImage String?
|
||||
urls String[]
|
||||
streamKey String? @unique
|
||||
|
||||
BIN
public/img/dashboard-dark.png
Normal file
BIN
public/img/dashboard-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 263 KiB |
@@ -22,14 +22,14 @@ const DashboardPage = ({ prop1 }: DashboardPageProps) => {
|
||||
<>
|
||||
<div className="md:hidden">
|
||||
<Image
|
||||
src="/examples/dashboard-light.png"
|
||||
src="/img/dashboard-light.png"
|
||||
width={1280}
|
||||
height={866}
|
||||
alt="Dashboard"
|
||||
className="block dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src="/examples/dashboard-dark.png"
|
||||
src="/img/dashboard-dark.png"
|
||||
width={1280}
|
||||
height={866}
|
||||
alt="Dashboard"
|
||||
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { toast } from "@/components/ui/use-toast";
|
||||
import { notice } from "@/lib/components/notifications/toast";
|
||||
import { type UserModel } from "@/lib/models";
|
||||
import { api } from "@/lib/utils/api";
|
||||
import { cn } from "@/lib/utils/styles";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import React from "react";
|
||||
@@ -29,11 +32,8 @@ const formSchema = z.object({
|
||||
.max(30, {
|
||||
message: "Username must not be longer than 30 characters.",
|
||||
}),
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.optional(),
|
||||
bio: z.string().max(160).min(0).optional(),
|
||||
email: z.string().email(),
|
||||
bio: z.string().max(160).min(0).nullable(),
|
||||
urls: z
|
||||
.array(
|
||||
z.object({
|
||||
@@ -47,8 +47,15 @@ type FormValues = z.infer<typeof formSchema>;
|
||||
const ProfileEditForm: React.FC<ProfileEditFormProps> = ({
|
||||
profile,
|
||||
}: ProfileEditFormProps) => {
|
||||
const updateUser = api.user.updateUser.useMutation({
|
||||
onSuccess: (result) => {
|
||||
console.log("profile-edit-form", "onSuccess", result);
|
||||
notice("Success", "Profile updated successfully");
|
||||
},
|
||||
});
|
||||
const defaultValues: Partial<FormValues> = {
|
||||
username: profile.username,
|
||||
email: profile.email || "",
|
||||
bio: profile.bio || "",
|
||||
urls: [],
|
||||
};
|
||||
@@ -63,8 +70,15 @@ const ProfileEditForm: React.FC<ProfileEditFormProps> = ({
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const onSubmit = (data: FormValues) => {
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
console.log("profile-edit-form", "onSubmit", data);
|
||||
await updateUser.mutateAsync({
|
||||
...profile,
|
||||
username: data.username,
|
||||
email: data.email,
|
||||
bio: data.bio,
|
||||
urls: [],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
@@ -8,14 +7,17 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ImageUpload from "@/lib/components/widgets/ImageUpload";
|
||||
import ImageUpload from "@/components/widgets/image-upload";
|
||||
import { notice } from "@/lib/components/notifications/toast";
|
||||
import { type UserModel } from "@/lib/models";
|
||||
import { api } from "@/lib/utils/api";
|
||||
import { useUploadThing } from "@/lib/utils/upload";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
const MAX_FILE_SIZE = 500000;
|
||||
const MAX_FILE_SIZE = 5242880;
|
||||
const ACCEPTED_IMAGE_TYPES = [
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
@@ -29,43 +31,71 @@ type ProfileImageEditFormProps = {
|
||||
const formSchema = z.object({
|
||||
profileImage: z
|
||||
.any()
|
||||
.refine(
|
||||
(file: File) => file?.size <= MAX_FILE_SIZE,
|
||||
`Max image size is 5MB.`
|
||||
)
|
||||
.refine((file: File) => {
|
||||
const ret = file?.size <= MAX_FILE_SIZE;
|
||||
console.log("profile-images-form", "profileImage_refine", ret);
|
||||
return ret;
|
||||
}, `Max image size is 5MB.`)
|
||||
.refine(
|
||||
(file: File) => ACCEPTED_IMAGE_TYPES.includes(file?.type),
|
||||
"Only .jpg, .jpeg, .png and .webp formats are supported."
|
||||
),
|
||||
headerImage: z
|
||||
.any()
|
||||
.refine(
|
||||
(file: File) => file?.size <= MAX_FILE_SIZE,
|
||||
`Max image size is 5MB.`
|
||||
)
|
||||
.refine(
|
||||
(file: File) => ACCEPTED_IMAGE_TYPES.includes(file?.type),
|
||||
"Only .jpg, .jpeg, .png and .webp formats are supported."
|
||||
),
|
||||
.optional(),
|
||||
// headerImage: z
|
||||
// .any()
|
||||
// .refine((file: File) => {
|
||||
// const ret = file === null ?? file?.size <= MAX_FILE_SIZE;
|
||||
// return ret;
|
||||
// }, `Max image size is 5MB.`)
|
||||
// .refine(
|
||||
// (file: File) => ACCEPTED_IMAGE_TYPES.includes(file?.type),
|
||||
// "Only .jpg, .jpeg, .png and .webp formats are supported."
|
||||
// )
|
||||
// .nullable()
|
||||
// .optional(),
|
||||
});
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
const ProfileImageEditForm: React.FC<ProfileImageEditFormProps> = ({
|
||||
profile,
|
||||
}) => {
|
||||
const updateUser = api.user.updateUser.useMutation({
|
||||
onSuccess: (result) => {
|
||||
console.log("profile-edit-form", "onSuccess", result);
|
||||
notice("Success", "Profile updated successfully");
|
||||
},
|
||||
});
|
||||
const { startUpload: uploadProfileImage } = useUploadThing({
|
||||
endpoint: "profileImageUpload",
|
||||
onUploadError: () => {
|
||||
alert("error occurred while uploading");
|
||||
},
|
||||
});
|
||||
|
||||
const defaultValues: Partial<FormValues> = {
|
||||
profileImage: profile.image,
|
||||
headerImage: profile.headerImage,
|
||||
profileImage: profile.profileImage,
|
||||
// headerImage: profile.headerImage,
|
||||
};
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues,
|
||||
mode: "onChange",
|
||||
});
|
||||
const onSubmit = (data: FormValues) => {
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
console.log("profile-images-form", "onSubmit", data);
|
||||
if (data.profileImage && data.profileImage instanceof File) {
|
||||
const results = await uploadProfileImage([data.profileImage]);
|
||||
if (results && results[0]) {
|
||||
const { fileUrl, fileKey } = results[0];
|
||||
await updateUser.mutateAsync({
|
||||
...profile,
|
||||
profileImage: fileUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Form {...form}>
|
||||
<p>{JSON.stringify(form.formState.errors, null, 2)}</p>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -73,9 +103,24 @@ const ProfileImageEditForm: React.FC<ProfileImageEditFormProps> = ({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Profile Image</FormLabel>
|
||||
<FormControl>
|
||||
<div className="w-1/2">
|
||||
<ImageUpload
|
||||
<div className="w-1/2">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name={"profileImage"}
|
||||
rules={{ required: "Gonna need a profile image" }}
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<ImageUpload
|
||||
{...field}
|
||||
imageUrl={value as string}
|
||||
onImageChanged={(image) => {
|
||||
onChange(image);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/* <ImageUpload
|
||||
onImageChanged={(image) => {
|
||||
console.log(
|
||||
"profile-images-form",
|
||||
@@ -83,9 +128,8 @@ const ProfileImageEditForm: React.FC<ProfileImageEditFormProps> = ({
|
||||
image
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
/> */}
|
||||
</div>
|
||||
<FormDescription>
|
||||
{
|
||||
"This is your avatar, it's how you will be recognised on the site"
|
||||
@@ -95,18 +139,14 @@ const ProfileImageEditForm: React.FC<ProfileImageEditFormProps> = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="headerImage"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Header Image</FormLabel>
|
||||
<FormControl>
|
||||
<ImageUpload
|
||||
onImageChanged={(image) => {
|
||||
console.log("profile-images-form", "onImageChanged", image);
|
||||
}}
|
||||
/>
|
||||
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is the image that will show at the top of your profile
|
||||
@@ -115,7 +155,7 @@ const ProfileImageEditForm: React.FC<ProfileImageEditFormProps> = ({
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
/> */}
|
||||
<Button type="submit">Update profile images</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
5
src/app/api/ping/route.ts
Normal file
5
src/app/api/ping/route.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export function GET() {
|
||||
return NextResponse.json({ ping: "pong" });
|
||||
}
|
||||
0
src/app/api/upload/routes.ts
Normal file
0
src/app/api/upload/routes.ts
Normal file
24
src/app/api/uploadthing/core.ts
Normal file
24
src/app/api/uploadthing/core.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createUploadthing, type FileRouter } from "uploadthing/next";
|
||||
|
||||
const f = createUploadthing();
|
||||
|
||||
const auth = async (req: Request) => ({ id: "fakeId" }); // Fake auth function
|
||||
|
||||
export const profileImageRouter = {
|
||||
profileImageUpload: f({ image: { maxFileSize: "4MB" } })
|
||||
.middleware(async (req) => {
|
||||
const user = await auth(req);
|
||||
|
||||
if (!user) throw new Error("Unauthorized");
|
||||
|
||||
return { userId: user.id };
|
||||
})
|
||||
.onUploadComplete(async ({ metadata, file }) => {
|
||||
// This code RUNS ON YOUR SERVER after upload
|
||||
console.log("Upload complete for userId:", metadata.userId);
|
||||
|
||||
console.log("file url", file.url);
|
||||
}),
|
||||
} satisfies FileRouter;
|
||||
|
||||
export type ProfileImageRouter = typeof profileImageRouter;
|
||||
7
src/app/api/uploadthing/route.ts
Normal file
7
src/app/api/uploadthing/route.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createNextRouteHandler } from "uploadthing/next";
|
||||
import { profileImageRouter } from "./core";
|
||||
|
||||
// Export routes for Next App Router
|
||||
export const { GET, POST } = createNextRouteHandler({
|
||||
router: profileImageRouter,
|
||||
});
|
||||
@@ -1,11 +1,13 @@
|
||||
import Navbar from "@/lib/components/layout/Navbar";
|
||||
import "@/styles/globals.css";
|
||||
import { type Metadata } from "next";
|
||||
import Providers from "./providers";
|
||||
import { fontSans } from "@/config/fonts";
|
||||
import Image from "next/image";
|
||||
import { cn } from "@/lib/utils/styles";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import Navbar from "@/lib/components/layout/Navbar";
|
||||
import { cn } from "@/lib/utils/styles";
|
||||
import { type Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import Providers from "./providers";
|
||||
//the order of these matters
|
||||
import "@uploadthing/react/styles.css";
|
||||
import "@/styles/globals.css";
|
||||
|
||||
const RootLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
@@ -19,14 +21,14 @@ const RootLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
<Providers>
|
||||
<div className="md:hidden">
|
||||
<Image
|
||||
src="/examples/dashboard-light.png"
|
||||
src="/img/dashboard-light.png"
|
||||
width={1280}
|
||||
height={866}
|
||||
alt="Dashboard"
|
||||
className="block dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src="/examples/dashboard-dark.png"
|
||||
src="/img/dashboard-dark.png"
|
||||
width={1280}
|
||||
height={866}
|
||||
alt="Dashboard"
|
||||
|
||||
94
src/components/widgets/image-upload.tsx
Normal file
94
src/components/widgets/image-upload.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React from "react";
|
||||
import Dropzone, { type DropzoneRef } from "react-dropzone";
|
||||
|
||||
type ImageUploadProps = {
|
||||
imageUrl: string | undefined;
|
||||
onImageChanged: (image: File) => void;
|
||||
};
|
||||
|
||||
const ImageUpload: React.FC<ImageUploadProps> = ({ onImageChanged }) => {
|
||||
const dropzoneRef = React.createRef<DropzoneRef>();
|
||||
|
||||
return (
|
||||
<Dropzone
|
||||
accept={{
|
||||
"image/png": [".png"],
|
||||
"image/jpg": [".jpg", ".jpeg"],
|
||||
}}
|
||||
maxFiles={1}
|
||||
ref={dropzoneRef}
|
||||
onDrop={(acceptedFiles) => {
|
||||
if (acceptedFiles.length !== 0 && acceptedFiles[0]) {
|
||||
onImageChanged(acceptedFiles[0]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ getRootProps, getInputProps, acceptedFiles }) => {
|
||||
return (
|
||||
<div className="w-64 h-64">
|
||||
<div
|
||||
{...getRootProps({ className: "dropzone" })}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{acceptedFiles?.length ? (
|
||||
<div
|
||||
id="preview"
|
||||
className="flex w-full items-center justify-center"
|
||||
>
|
||||
{acceptedFiles[0] && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
className="object-cover"
|
||||
src={URL.createObjectURL(acceptedFiles[0])}
|
||||
alt="image preview"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div id="drop">
|
||||
<label
|
||||
htmlFor="dropzone-file"
|
||||
className="dark:hover:bg-bray-800 flex h-64 w-full cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 hover:bg-gray-100 dark:border-gray-600 dark:bg-slate-700 dark:hover:border-gray-500 dark:hover:bg-gray-600"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center pb-6 pt-5">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="mb-3 h-10 w-10 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
></path>
|
||||
</svg>
|
||||
<p className="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="font-semibold">Click to upload</span>{" "}
|
||||
or drag and drop
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
SVG, PNG, JPG or GIF (MAX. 800x400px)
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
{...getInputProps()}
|
||||
id="dropzone-file"
|
||||
type="file"
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Dropzone>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUpload;
|
||||
@@ -26,20 +26,20 @@ const UserNav: React.FC<UserNavProps> = ({ session }) => {
|
||||
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage
|
||||
src={session.user.image || "/img/default-avatar.png"}
|
||||
src={session.user.profileImage || "/img/default-avatar.png"}
|
||||
alt="User Avatar"
|
||||
/>
|
||||
<AvatarFallback>SC</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="end" forceMount>
|
||||
<DropdownMenuContent className="w-56 bg-muted" align="end" forceMount>
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{session.user.name}
|
||||
</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">
|
||||
<p className="text-xs leading-none ">
|
||||
{session.user.email}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@ const Chat = ({ show }: ChatProps) => {
|
||||
{messages.map((item) => (
|
||||
<ChatItem
|
||||
key={item.id}
|
||||
img={item.fromUser.image}
|
||||
img={item.fromUser.profileImage}
|
||||
date={item.timestamp}
|
||||
from={item.fromUser.name}
|
||||
message={item.message}
|
||||
|
||||
@@ -36,8 +36,8 @@ const DashboardSidebar = ({ session }: DashboardSidebarProps) => {
|
||||
return (
|
||||
<div className="h-full w-60 space-y-2 p-3 ">
|
||||
<div className="flex items-center space-x-4 p-2">
|
||||
{session.user.image && (
|
||||
<UserImage src={session.user.image} status={"offline"} size={"md"} />
|
||||
{session.user.profileImage && (
|
||||
<UserImage src={session.user.profileImage} status={"offline"} size={"md"} />
|
||||
)}
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold">
|
||||
|
||||
@@ -58,7 +58,7 @@ const ShowPlayerPage = ({ title, show }: ShowPlayerPageProps) => {
|
||||
<div className="flex items-center justify-between bg-white px-4 py-4 dark:bg-slate-800">
|
||||
<div className="flex min-w-0 items-center">
|
||||
<UserImage
|
||||
src={show.user.image || ""}
|
||||
src={show.user.profileImage || ""}
|
||||
status={"offline"}
|
||||
size={"md"}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
type UserModel = {
|
||||
username: string;
|
||||
name: string;
|
||||
bio: string | undefined;
|
||||
image: string | undefined;
|
||||
headerImage: string | undefined;
|
||||
sites: string[] | undefined;
|
||||
email: string;
|
||||
bio: string | null;
|
||||
profileImage: string | null;
|
||||
headerImage: string | null;
|
||||
urls: string[];
|
||||
};
|
||||
export default UserModel;
|
||||
|
||||
@@ -9,8 +9,11 @@ const mapAuthUserToUserModel = (
|
||||
? {
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
bio: user.bio,
|
||||
image: user.image,
|
||||
profileImage: user.profileImage,
|
||||
headerImage: user.headerImage,
|
||||
urls: [],
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -21,8 +24,11 @@ const mapDbAuthUserToUserModel = (
|
||||
? {
|
||||
username: user.username ?? "unknownuser",
|
||||
name: user.name ?? "Unknown User",
|
||||
email: user.email,
|
||||
bio: user.bio,
|
||||
image: user.image,
|
||||
profileImage: user.profileImage,
|
||||
headerImage: user.headerImage,
|
||||
urls: [],
|
||||
}
|
||||
: undefined;
|
||||
|
||||
|
||||
5
src/lib/utils/upload.ts
Normal file
5
src/lib/utils/upload.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { type ProfileImageRouter } from "@/app/api/uploadthing/core";
|
||||
import { generateReactHelpers } from "@uploadthing/react/hooks";
|
||||
|
||||
export const { useUploadThing, uploadFiles } =
|
||||
generateReactHelpers<ProfileImageRouter>();
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
mapAuthUserToUserModel,
|
||||
mapDbAuthUserToUserModel,
|
||||
} from "@/lib/utils/mappers/userMapper";
|
||||
import { mapDbAuthUserToUserModel } from "@/lib/utils/mappers/userMapper";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
@@ -47,4 +44,43 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return user;
|
||||
}),
|
||||
updateUser: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
username: z.string(),
|
||||
email: z.string().email(),
|
||||
bio: z.string().nullable(),
|
||||
urls: z.array(z.string()),
|
||||
profileImage: z.string().nullable(),
|
||||
headerImage: z.string().nullable(),
|
||||
})
|
||||
)
|
||||
.mutation(
|
||||
async ({
|
||||
input: { username, email, bio, urls, profileImage, headerImage },
|
||||
ctx,
|
||||
}) => {
|
||||
const user = await ctx.prisma.user.findUnique({
|
||||
where: { id: ctx.session.id },
|
||||
});
|
||||
if (!user) {
|
||||
throw new trpc.TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "User is not authenticated.",
|
||||
});
|
||||
}
|
||||
await ctx.prisma.user.update({
|
||||
where: { id: ctx.session.id },
|
||||
data: {
|
||||
...user,
|
||||
username,
|
||||
email,
|
||||
bio,
|
||||
urls,
|
||||
profileImage,
|
||||
headerImage,
|
||||
},
|
||||
});
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { getServerSession, type NextAuthOptions } from "next-auth";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import { PrismaAdapter } from "@next-auth/prisma-adapter";
|
||||
import { env } from "@/env.mjs";
|
||||
import { mapDbAuthUserToUserModel } from "@/lib/utils/mappers/userMapper";
|
||||
import { prisma } from "@/server/db";
|
||||
import { PrismaAdapter } from "@next-auth/prisma-adapter";
|
||||
import { verify } from "argon2";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import {
|
||||
getServerSession,
|
||||
type NextAuthOptions,
|
||||
type Session,
|
||||
type User,
|
||||
} from "next-auth";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
session: {
|
||||
@@ -13,21 +19,26 @@ export const authOptions: NextAuthOptions = {
|
||||
},
|
||||
callbacks: {
|
||||
session: ({ session, token }) => {
|
||||
if (token) {
|
||||
session.id = token.id as string;
|
||||
session.id = token.id as string;
|
||||
}
|
||||
|
||||
return session;
|
||||
return {
|
||||
id: token.id,
|
||||
user: token.user,
|
||||
} as Session;
|
||||
},
|
||||
jwt: ({ token, user }) => {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
token.email = user.email;
|
||||
token.name = user.name;
|
||||
token.bio = user.bio;
|
||||
jwt: async ({ token }) => {
|
||||
if (!token.email) {
|
||||
return token;
|
||||
}
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: token.email,
|
||||
},
|
||||
});
|
||||
if (user) {
|
||||
return {
|
||||
id: token.sub,
|
||||
user: mapDbAuthUserToUserModel(user),
|
||||
};
|
||||
}
|
||||
|
||||
return token;
|
||||
},
|
||||
},
|
||||
@@ -48,35 +59,40 @@ export const authOptions: NextAuthOptions = {
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
authorize: async (credentials) => {
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
try {
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { email: credentials.email },
|
||||
});
|
||||
|
||||
if (!user || !user.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isValidPassword = await verify(
|
||||
user.password,
|
||||
credentials.password
|
||||
);
|
||||
|
||||
if (!isValidPassword) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
bio: user.bio,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
profileImage: user.profileImage,
|
||||
headerImage: user.headerImage,
|
||||
} as User;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { email: credentials.email },
|
||||
});
|
||||
|
||||
if (!user || !user.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isValidPassword = await verify(
|
||||
user.password,
|
||||
credentials.password
|
||||
);
|
||||
|
||||
if (!isValidPassword) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
bio: user.bio,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
image: user.image,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -37,23 +37,22 @@
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #0E2954;
|
||||
--background: #0e2954;
|
||||
--foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--card: #1F6E8C;
|
||||
--card: #1f6e8c;
|
||||
--card-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: #2E8A99;
|
||||
--muted-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--accent: #84A7A1;
|
||||
--muted: #2e8a99;
|
||||
--muted-foreground: #374151;
|
||||
--accent: #84a7a1;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 224 71% 4%;
|
||||
--popover-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--border: 216 34% 17%;
|
||||
--input: 216 34% 17%;
|
||||
--border: #374151;
|
||||
--input: #374151;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 1.2%;
|
||||
@@ -64,7 +63,7 @@
|
||||
--destructive: 0 63% 31%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: 216 34% 17%;
|
||||
--ring: #374151;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ const config: Config = {
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
border: "var(--border)",
|
||||
input: "var(--input)",
|
||||
ring: "var(--ring)",
|
||||
background: "var(--background)",
|
||||
foreground: "var(--foreground)",
|
||||
primary: {
|
||||
@@ -33,7 +33,7 @@ const config: Config = {
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "var(--muted)",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
foreground: "var(--muted-foreground)",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "var(--accent)",
|
||||
|
||||
10
types/next-auth.d.ts
vendored
10
types/next-auth.d.ts
vendored
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import NextAuth from "next-auth";
|
||||
|
||||
declare module "next-auth" {
|
||||
@@ -8,10 +9,11 @@ declare module "next-auth" {
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image: string;
|
||||
bio: string;
|
||||
username: string;
|
||||
name: string;
|
||||
bio: string;
|
||||
email: string;
|
||||
profileImage: string;
|
||||
headerImage: string;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user