From 8db84391f9a438a56929a787ebd266fe91b416ef Mon Sep 17 00:00:00 2001 From: quentingrchr Date: Wed, 23 Oct 2024 15:25:04 +0200 Subject: [PATCH 1/5] add prisma migration check --- .github/workflows/code-chek.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/code-chek.yml diff --git a/.github/workflows/code-chek.yml b/.github/workflows/code-chek.yml new file mode 100644 index 0000000..d29e24b --- /dev/null +++ b/.github/workflows/code-chek.yml @@ -0,0 +1,27 @@ +name: Check code + +on: + pull_request: + branches: + - main + +jobs: + check-migrations: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Check Prisma Migrations + uses: premieroctet/prisma-drop-migration-warning@main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + main-branch: 'main' + path: 'prisma' + warning: true \ No newline at end of file From 22d6d173b9c232389767f6386fa5db720f3f96b9 Mon Sep 17 00:00:00 2001 From: quentingrchr Date: Thu, 24 Oct 2024 16:21:12 +0200 Subject: [PATCH 2/5] feat: add avatar input --- prisma/json-schema/json-schema.json | 6 ++ .../migration.sql | 2 + prisma/schema.prisma | 1 + src/components/Avatar.tsx | 34 +++++++---- .../teams/form/settings/AvatarField.tsx | 59 +++++++++++++++++++ .../teams/form/settings/SettingsField.tsx | 5 +- .../teams/form/settings/TeamInfo.tsx | 18 ++++-- .../teams/form/settings/form-data.tsx | 9 +-- 8 files changed, 110 insertions(+), 24 deletions(-) create mode 100644 prisma/migrations/20241024134534_add_team_avatar/migration.sql create mode 100644 src/components/teams/form/settings/AvatarField.tsx diff --git a/prisma/json-schema/json-schema.json b/prisma/json-schema/json-schema.json index a1ebcc5..5c1c113 100644 --- a/prisma/json-schema/json-schema.json +++ b/prisma/json-schema/json-schema.json @@ -179,6 +179,12 @@ "id": { "type": "string" }, + "avatar": { + "type": [ + "string", + "null" + ] + }, "name": { "type": "string" }, diff --git a/prisma/migrations/20241024134534_add_team_avatar/migration.sql b/prisma/migrations/20241024134534_add_team_avatar/migration.sql new file mode 100644 index 0000000..142ea83 --- /dev/null +++ b/prisma/migrations/20241024134534_add_team_avatar/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "teams" ADD COLUMN "avatar" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 405e5d4..f9b19d6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -95,6 +95,7 @@ model VerificationToken { model Team { id String @id @default(uuid()) + avatar String? name String slug String memberships Membership[] diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 1eebd66..06643e7 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -1,25 +1,29 @@ +import cn from 'classnames'; import Image from 'next/image'; -import React from 'react'; interface IProps { - size: 'xs' | 'sm' | 'md' | 'lg'; + size: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'; name?: string; src?: string; } export default function Avatar({ size = 'md', src, name }: IProps) { const sizeClass: Record = { - xs: 'h-4 w-4', - sm: 'h-6 w-6', - md: 'h-8 w-8', - lg: 'h-10 w-10', + 'xs': 'h-4 w-4', + 'sm': 'h-6 w-6', + 'md': 'h-8 w-8', + 'lg': 'h-10 w-10', + 'xl': 'h-16 w-16', + '2xl': 'h-32 w-32', }; const sizePixels: Record = { - xs: 4, - sm: 6, - md: 8, - lg: 10, + 'xs': 4, + 'sm': 6, + 'md': 8, + 'lg': 10, + 'xl': 16, + '2xl': 32, }; if (src !== undefined) { @@ -39,7 +43,15 @@ export default function Avatar({ size = 'md', src, name }: IProps) { - + {name .split(' ') .splice(0, 2) diff --git a/src/components/teams/form/settings/AvatarField.tsx b/src/components/teams/form/settings/AvatarField.tsx new file mode 100644 index 0000000..8383a33 --- /dev/null +++ b/src/components/teams/form/settings/AvatarField.tsx @@ -0,0 +1,59 @@ +import Avatar from '@/components/Avatar'; +import Button from '@/components/Button'; +import { useRef } from 'react'; + +interface IProps { + avatar?: string; + name?: string; +} +export default function AvatarField({ avatar, name }: IProps) { + const fileInputRef = useRef(null); + + function onAvatarChange(file: File) {} + function onAvatarRemove() {} + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + fileInputRef.current?.click(); + }; + + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + onAvatarChange?.(file); + } + }; + + const handleRemove = (e: React.MouseEvent) => { + e.preventDefault(); + onAvatarRemove?.(); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + return ( +
+ +
+ +
+ + + +
+
+
+ ); +} diff --git a/src/components/teams/form/settings/SettingsField.tsx b/src/components/teams/form/settings/SettingsField.tsx index e2b0066..fc17041 100644 --- a/src/components/teams/form/settings/SettingsField.tsx +++ b/src/components/teams/form/settings/SettingsField.tsx @@ -1,7 +1,6 @@ import { Input, Select, TextArea } from '@/components/Input'; -import React from 'react'; import { useFormContext } from 'react-hook-form'; -import { FieldData } from './form-data'; +import { TextFieldData } from './form-data'; export default function SettingsField({ id, @@ -16,7 +15,7 @@ export default function SettingsField({ selectDefault, selectOptions, maxLength, -}: FieldData) { +}: TextFieldData) { const { register, formState: { errors }, diff --git a/src/components/teams/form/settings/TeamInfo.tsx b/src/components/teams/form/settings/TeamInfo.tsx index cf40478..35c3e33 100644 --- a/src/components/teams/form/settings/TeamInfo.tsx +++ b/src/components/teams/form/settings/TeamInfo.tsx @@ -1,16 +1,17 @@ 'use client'; +import updateTeamInfo from '@/actions/update-team-info'; +import Button from '@/components/Button'; +import useCustomToast from '@/hooks/useCustomToast'; +import useTransitionRefresh from '@/hooks/useTransitionRefresh'; import { Team } from '@prisma/client'; import { FormEvent, useTransition } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { DangerZoneTeam } from '../DangerZone'; -import useCustomToast from '@/hooks/useCustomToast'; +import AvatarField from './AvatarField'; import SettingsField from './SettingsField'; -import { fieldsData, FieldName, FIELDS } from './form-data'; -import Button from '@/components/Button'; -import updateTeamInfo from '@/actions/update-team-info'; import TeamColorField from './TeamColorField'; -import useTransitionRefresh from '@/hooks/useTransitionRefresh'; +import { FIELDS, FieldName, textFieldsData } from './form-data'; const PRO_FIELDS = ['prompt']; @@ -23,6 +24,7 @@ const TeamInfo = ({ team }: { team: Team }) => { const methods = useForm({ mode: 'onBlur', defaultValues: { + [FIELDS.avatar]: '', [FIELDS.bio]: team?.bio || '', [FIELDS.name]: team?.name || '', [FIELDS.website]: team?.website || '', @@ -66,7 +68,11 @@ const TeamInfo = ({ team }: { team: Team }) => {
- {fieldsData + + {textFieldsData .filter( (field) => team?.subscriptionId || !PRO_FIELDS?.includes(field?.id) diff --git a/src/components/teams/form/settings/form-data.tsx b/src/components/teams/form/settings/form-data.tsx index 0677b7e..465361e 100644 --- a/src/components/teams/form/settings/form-data.tsx +++ b/src/components/teams/form/settings/form-data.tsx @@ -1,10 +1,11 @@ +import { SelectOptionProps } from '@/components/Input'; import { ImGithub } from '@react-icons/all-files/im/ImGithub'; -import { ImTwitter } from '@react-icons/all-files/im/ImTwitter'; import { ImLink } from '@react-icons/all-files/im/ImLink'; +import { ImTwitter } from '@react-icons/all-files/im/ImTwitter'; import { RegisterOptions } from 'react-hook-form'; -import { SelectOptionProps } from '@/components/Input'; export const FIELDS = { + avatar: 'avatar', bio: 'bio', name: 'name', website: 'website', @@ -16,7 +17,7 @@ export const FIELDS = { export type FieldName = (typeof FIELDS)[keyof typeof FIELDS]; -export interface FieldData { +export interface TextFieldData { id: FieldName; input: 'text' | 'textarea' | 'select'; inputType: 'text' | 'email' | 'password' | 'url'; @@ -32,7 +33,7 @@ export interface FieldData { maxLength?: number; } -export const fieldsData: FieldData[] = [ +export const textFieldsData: TextFieldData[] = [ { id: FIELDS.name, input: 'text', From 6efb01fe6a8623be907363ba6620ae1309bdd8f7 Mon Sep 17 00:00:00 2001 From: quentingrchr Date: Thu, 24 Oct 2024 16:21:28 +0200 Subject: [PATCH 3/5] feat: add avatar input --- src/components/teams/form/settings/SettingsForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/teams/form/settings/SettingsForm.tsx b/src/components/teams/form/settings/SettingsForm.tsx index 78fadb5..726dd46 100644 --- a/src/components/teams/form/settings/SettingsForm.tsx +++ b/src/components/teams/form/settings/SettingsForm.tsx @@ -8,9 +8,9 @@ import { Team } from '@prisma/client'; import { FormEvent, useTransition } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { DangerZoneTeam } from '../DangerZone'; -import { FieldName, FIELDS, fieldsData } from './form-data'; import SettingsField from './SettingsField'; import TeamColorField from './TeamColorField'; +import { FIELDS, FieldName, textFieldsData } from './form-data'; const PRO_FIELDS = ['prompt']; @@ -65,7 +65,7 @@ const SettingsForm = ({ team }: { team: Team }) => {
- {fieldsData + {textFieldsData .filter( (field) => team?.subscriptionId || !PRO_FIELDS?.includes(field?.id) From 1109f4b8c4a656b235ffae8a2fa65395056e84e3 Mon Sep 17 00:00:00 2001 From: quentingrchr Date: Thu, 24 Oct 2024 16:23:08 +0200 Subject: [PATCH 4/5] remove unwanted file --- .github/workflows/code-chek.yml | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 .github/workflows/code-chek.yml diff --git a/.github/workflows/code-chek.yml b/.github/workflows/code-chek.yml deleted file mode 100644 index d29e24b..0000000 --- a/.github/workflows/code-chek.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Check code - -on: - pull_request: - branches: - - main - -jobs: - check-migrations: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - with: - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha }} - - - name: Check Prisma Migrations - uses: premieroctet/prisma-drop-migration-warning@main - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - main-branch: 'main' - path: 'prisma' - warning: true \ No newline at end of file From 9bc62d3a1d42394067b36eb1a1cff0bf6cfc624a Mon Sep 17 00:00:00 2001 From: quentingrchr Date: Fri, 25 Oct 2024 18:40:17 +0200 Subject: [PATCH 5/5] update docker and add upload avatar feature --- .gitignore | 3 + docker-compose.yml | 20 +++ next.config.js | 12 ++ package.json | 1 + src/actions/update-team-info.ts | 120 +++++++++++++++- src/components/Avatar.tsx | 32 +++-- .../teams/form/settings/AvatarField.tsx | 96 ++++++++----- .../teams/form/settings/SettingsForm.tsx | 105 -------------- .../teams/form/settings/TeamInfo.tsx | 135 +++++++++++++++--- .../teams/form/settings/form-data.tsx | 2 + yarn.lock | 7 + 11 files changed, 355 insertions(+), 178 deletions(-) delete mode 100644 src/components/teams/form/settings/SettingsForm.tsx diff --git a/.gitignore b/.gitignore index ad4c378..863cd80 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ next.config.original.js #Content Layer .contentlayer + +#Uploads +/uploads \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 272204d..fb76988 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,26 @@ services: ports: - '1080:80' - '25:25' + + web: + image: node:18 + working_dir: /app + command: sh -c "yarn && npm run dev" # Install dependencies before starting + ports: + - "3000:3000" + volumes: + - ./uploads:/app/public/uploads # Mount local uploads to Next.js public directory + - .:/app # Mount entire project for development + - /app/node_modules # Prevent overriding node_modules + environment: + NODE_ENV: development + PORT: 3000 + env_file: + - .env # This will load environment variables from .env file + depends_on: + - postgres + volumes: postgresql: postgresql_data: + # uploads: ajouter point de volume pour uploads diff --git a/next.config.js b/next.config.js index 1aac90f..011f53a 100644 --- a/next.config.js +++ b/next.config.js @@ -12,6 +12,18 @@ const nextConfig = { serverActions: true, serverComponentsExternalPackages: ['mjml', 'mjml-react'], }, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'digestclub.com', + }, + { + protocol: 'http', + hostname: 'localhost', + }, + ], + }, reactStrictMode: false, }; diff --git a/package.json b/package.json index f0b95ee..835a6f1 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "feed": "^4.2.2", "framer-motion": "^10.7.0", "husky": "^8.0.3", + "image-size": "^1.1.1", "jsonwebtoken": "^9.0.1", "lodash": "^4.17.21", "lru-cache": "^10.0.2", diff --git a/src/actions/update-team-info.ts b/src/actions/update-team-info.ts index 59319c1..ea2ae42 100644 --- a/src/actions/update-team-info.ts +++ b/src/actions/update-team-info.ts @@ -1,9 +1,11 @@ 'use server'; +import { FIELDS } from '@/components/teams/form/settings/form-data'; import db from '@/lib/db'; import * as Sentry from '@sentry/nextjs'; +import { mkdir, unlink, writeFile } from 'fs/promises'; +import { imageSize } from 'image-size'; +import { basename, extname, join } from 'path'; import { checkAuthAction, checkTeamAction, getErrorMessage } from './utils'; -import { Team } from '@prisma/client'; - interface UpdateTeamInfoResult { error?: { message: string; @@ -13,19 +15,129 @@ interface UpdateTeamInfoResult { }; } +async function updateAvatarFile( + dirPath: string, + fileName: string, + buffer: Buffer, + oldUrl?: string +) { + const path = join(dirPath, fileName); + + if (oldUrl) { + const oldFilname = basename(oldUrl); + const oldPath = join(dirPath, oldFilname); + + await unlink(oldPath); + } + + await writeFile(path, buffer); +} + +/** + * Upload an avatar file and link it to a team. If an old avatar exists, delete it. + * @param file The file to upload and link to the team + * @param teamId The team ID to link the file to + * @param oldAvatar The old avatar (path) to delete if it exists + * @returns + */ +async function updateAvatar(file: File, teamId: string, oldAvatar?: string) { + const MAX_FILE_SIZE_MB = 5; + const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024; + const MIN_WIDTH = 100; // minimum width and height in pixels + + if (file.size > MAX_FILE_SIZE_BYTES) { + return new Error(`File size must be less than ${MAX_FILE_SIZE_MB}MB.`); + } + + const fileExtension = extname(file.name); + const randomString = new Date().getTime().toString(); + const fileName = `${randomString}${fileExtension}`; + const dirPath = join(process.cwd(), 'uploads', teamId, 'avatar'); + const bytes = await file.arrayBuffer(); + const buffer = Buffer.from(bytes); + const dimensions = imageSize(buffer); + + if (!dimensions.width || !dimensions.height) { + throw new Error('Unable to determine image dimensions.'); + } + + if (dimensions.width < MIN_WIDTH || dimensions.height < MIN_WIDTH) { + throw new Error(`Image must be at least ${MIN_WIDTH}x${MIN_WIDTH} pixels.`); + } + + if (dimensions.width !== dimensions.height) { + throw new Error('Image must be square.'); + } + + await mkdir(dirPath, { recursive: true }); + + await updateAvatarFile(dirPath, fileName, buffer, oldAvatar); + + await db.team.update({ + where: { id: teamId }, + data: { + avatar: `${process.env.NEXT_PUBLIC_PUBLIC_URL}/uploads/${teamId}/avatar/${fileName}`, // url to the avatar + }, + }); +} + export default async function updateTeamInfo( - updatedTeamInfo: Partial, + updatedTeamInfo: FormData, teamId: string ): Promise { try { await checkAuthAction(); await checkTeamAction(teamId); + const team = await db.team.findUnique({ + where: { id: teamId }, + select: { avatar: true }, + }); + + const avatarFile = updatedTeamInfo.get(FIELDS.avatarUpload) as File; + const name = updatedTeamInfo.get(FIELDS.avatar) as string | null; + const bio = updatedTeamInfo.get(FIELDS.bio) as string | null; + const website = updatedTeamInfo.get(FIELDS.website) as string | null; + const github = updatedTeamInfo.get(FIELDS.github) as string | null; + const twitter = updatedTeamInfo.get(FIELDS.twitter) as string | null; + const color = updatedTeamInfo.get(FIELDS.color) as string | null; + const prompt = updatedTeamInfo.get(FIELDS.prompt) as string | null; + + const updatedFields: any = {}; + + if (name) { + updatedFields['name'] = name; + } + if (bio) { + updatedFields['bio'] = bio; + } + if (website) { + updatedFields['website'] = website; + } + if (github) { + updatedFields['github'] = github; + } + if (twitter) { + updatedFields['twitter'] = twitter; + } + if (color) { + updatedFields['color'] = color; + } + if (prompt) { + updatedFields['prompt'] = prompt; + } + const updatedTeam = await db.team.update({ where: { id: teamId }, - data: updatedTeamInfo, + data: { + ...updatedFields, + }, }); + if (avatarFile) { + await updateAvatar(avatarFile, teamId, team?.avatar ?? undefined); + } + return { data: { team: JSON.stringify(updatedTeam), diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 06643e7..020d0ed 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -1,5 +1,6 @@ import cn from 'classnames'; import Image from 'next/image'; +import { useState } from 'react'; interface IProps { size: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'; @@ -8,32 +9,37 @@ interface IProps { } export default function Avatar({ size = 'md', src, name }: IProps) { + const [isImageError, setIsImageError] = useState(false); const sizeClass: Record = { - 'xs': 'h-4 w-4', - 'sm': 'h-6 w-6', - 'md': 'h-8 w-8', - 'lg': 'h-10 w-10', - 'xl': 'h-16 w-16', - '2xl': 'h-32 w-32', + 'xs': 'h-4 w-4' /* 16px */, + 'sm': 'h-6 w-6' /* 24px */, + 'md': 'h-8 w-8' /* 32px */, + 'lg': 'h-10 w-10' /* 40px */, + 'xl': 'h-16 w-16' /* 64px */, + '2xl': 'h-32 w-32' /* 128px */, }; const sizePixels: Record = { - 'xs': 4, - 'sm': 6, - 'md': 8, - 'lg': 10, - 'xl': 16, - '2xl': 32, + 'xs': 16, + 'sm': 24, + 'md': 32, + 'lg': 40, + 'xl': 64, + '2xl': 128, }; - if (src !== undefined) { + if (src !== undefined && !isImageError) { return ( avatar { + setIsImageError(true); + }} /> ); } diff --git a/src/components/teams/form/settings/AvatarField.tsx b/src/components/teams/form/settings/AvatarField.tsx index 8383a33..8c40884 100644 --- a/src/components/teams/form/settings/AvatarField.tsx +++ b/src/components/teams/form/settings/AvatarField.tsx @@ -1,58 +1,84 @@ import Avatar from '@/components/Avatar'; import Button from '@/components/Button'; -import { useRef } from 'react'; +import { ChangeEvent, useRef, useState } from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import { FIELDS } from './form-data'; interface IProps { avatar?: string; name?: string; + teamId?: string; } -export default function AvatarField({ avatar, name }: IProps) { +export default function AvatarField({ avatar, name, teamId }: IProps) { const fileInputRef = useRef(null); - - function onAvatarChange(file: File) {} - function onAvatarRemove() {} + const [file, setFile] = useState(null); + const { control, setValue } = useFormContext(); const handleClick = (e: React.MouseEvent) => { e.preventDefault(); fileInputRef.current?.click(); }; - const handleFileChange = (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (file) { - onAvatarChange?.(file); - } - }; - - const handleRemove = (e: React.MouseEvent) => { - e.preventDefault(); - onAvatarRemove?.(); - if (fileInputRef.current) { - fileInputRef.current.value = ''; + const handleFileChange = ( + event: ChangeEvent, + onChange: (file: File | null) => void + ) => { + const selectedFile = event.target.files?.[0] || null; + onChange(selectedFile); // Update the form state + if (selectedFile) { + setFile(selectedFile); + } else { + setFile(null); } }; return (
- +

Avatar

- -
- - - -
+ ( + <> + +
+ + handleFileChange(e, onChange)} // Pass onChange from Controller + className="hidden" + aria-label="Upload avatar" + /> + + +
+ + )} + />
); diff --git a/src/components/teams/form/settings/SettingsForm.tsx b/src/components/teams/form/settings/SettingsForm.tsx deleted file mode 100644 index 726dd46..0000000 --- a/src/components/teams/form/settings/SettingsForm.tsx +++ /dev/null @@ -1,105 +0,0 @@ -'use client'; - -import updateTeamInfo from '@/actions/update-team-info'; -import Button from '@/components/Button'; -import useCustomToast from '@/hooks/useCustomToast'; -import useTransitionRefresh from '@/hooks/useTransitionRefresh'; -import { Team } from '@prisma/client'; -import { FormEvent, useTransition } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; -import { DangerZoneTeam } from '../DangerZone'; -import SettingsField from './SettingsField'; -import TeamColorField from './TeamColorField'; -import { FIELDS, FieldName, textFieldsData } from './form-data'; - -const PRO_FIELDS = ['prompt']; - -type SettingsForm = Record; - -const SettingsForm = ({ team }: { team: Team }) => { - const { successToast, errorToast } = useCustomToast(); - const [isPending, startTransition] = useTransition(); - - const methods = useForm({ - mode: 'onBlur', - defaultValues: { - [FIELDS.bio]: team?.bio || '', - [FIELDS.name]: team?.name || '', - [FIELDS.website]: team?.website || '', - [FIELDS.github]: team?.github || '', - [FIELDS.twitter]: team?.twitter || '', - [FIELDS.color]: team?.color || '#6d28d9', - [FIELDS.prompt]: team?.prompt || '', - }, - }); - - const { - handleSubmit, - reset, - formState: { isDirty, dirtyFields }, - } = methods; - const { refresh, isRefreshing } = useTransitionRefresh(); - - const onSubmit = (e: FormEvent) => - startTransition(async () => { - handleSubmit(async (values) => { - let changedValues: Partial = {}; - Object.keys(dirtyFields).map((key) => { - changedValues[key as FieldName] = values[key as FieldName]; - }); - const { error } = await updateTeamInfo(changedValues, team?.id); - if (error) { - errorToast(error.message); - } else { - successToast('Team info updated successfully'); - refresh(); - } - - reset({}, { keepValues: true }); - })(e); - }); - - return ( - - {/* @ts-expect-error */} - -
-
- {textFieldsData - .filter( - (field) => - team?.subscriptionId || !PRO_FIELDS?.includes(field?.id) - ) - .map((field) => ( - - ))} - - - -
-
- -
-
- -
-
-
-
- -
- ); -}; - -export default SettingsForm; diff --git a/src/components/teams/form/settings/TeamInfo.tsx b/src/components/teams/form/settings/TeamInfo.tsx index 35c3e33..a335d98 100644 --- a/src/components/teams/form/settings/TeamInfo.tsx +++ b/src/components/teams/form/settings/TeamInfo.tsx @@ -5,17 +5,35 @@ import Button from '@/components/Button'; import useCustomToast from '@/hooks/useCustomToast'; import useTransitionRefresh from '@/hooks/useTransitionRefresh'; import { Team } from '@prisma/client'; -import { FormEvent, useTransition } from 'react'; +import { ImGithub } from '@react-icons/all-files/im/ImGithub'; +import { ImLink } from '@react-icons/all-files/im/ImLink'; +import { ImTwitter } from '@react-icons/all-files/im/ImTwitter'; +import { FormEvent, useEffect, useTransition } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { DangerZoneTeam } from '../DangerZone'; import AvatarField from './AvatarField'; import SettingsField from './SettingsField'; import TeamColorField from './TeamColorField'; -import { FIELDS, FieldName, textFieldsData } from './form-data'; +import { FIELDS, FieldName } from './form-data'; -const PRO_FIELDS = ['prompt']; +/* Represents the form data with fields names perfectly matching the Team model */ +interface BasicSettingsForm { + [FIELDS.avatar]: string; + [FIELDS.bio]: string; + [FIELDS.name]: string; + [FIELDS.website]: string; + [FIELDS.github]: string; + [FIELDS.twitter]: string; + [FIELDS.color]: string; + [FIELDS.prompt]: string; +} -type SettingsForm = Record; +interface InternalUploadSettingsForm { + [FIELDS.avatarUpload]: File; + [FIELDS.avatarRemove]: string; +} + +interface SettingsForm extends BasicSettingsForm, InternalUploadSettingsForm {} const TeamInfo = ({ team }: { team: Team }) => { const { successToast, errorToast } = useCustomToast(); @@ -24,7 +42,7 @@ const TeamInfo = ({ team }: { team: Team }) => { const methods = useForm({ mode: 'onBlur', defaultValues: { - [FIELDS.avatar]: '', + [FIELDS.avatar]: team?.avatar || undefined, [FIELDS.bio]: team?.bio || '', [FIELDS.name]: team?.name || '', [FIELDS.website]: team?.website || '', @@ -38,18 +56,33 @@ const TeamInfo = ({ team }: { team: Team }) => { const { handleSubmit, reset, + watch, + getValues, formState: { isDirty, dirtyFields }, } = methods; const { refresh, isRefreshing } = useTransitionRefresh(); + // Watch the entire form or specific fields + const formValues = watch(); // This will watch all form values + + // Use useEffect to log form values whenever they change + useEffect(() => { + console.log('Form values changed:', formValues); + }, [formValues]); // This effect will run every time formV + const onSubmit = (e: FormEvent) => startTransition(() => { handleSubmit(async (values) => { - let changedValues: Partial = {}; + let formData = new FormData(); + + // Map to the dirty fields (fields that have been changed) and append to the form data Object.keys(dirtyFields).map((key) => { - changedValues[key as FieldName] = values[key as FieldName]; + const k = key as FieldName; + const v = values[k]; + formData.append(k, v); }); - const { error } = await updateTeamInfo(changedValues, team?.id); + + const { error } = await updateTeamInfo(formData, team?.id); if (error) { errorToast(error.message); } else { @@ -69,21 +102,81 @@ const TeamInfo = ({ team }: { team: Team }) => {
+ + + + + + } + placeholder="https://company.io" + defaultValue={team?.website || ''} + /> + + } + prefix="@" + placeholder="" + defaultValue={team?.github || ''} + /> + + } + prefix="@" + placeholder="" + defaultValue={team?.twitter || ''} /> - {textFieldsData - .filter( - (field) => - team?.subscriptionId || !PRO_FIELDS?.includes(field?.id) - ) - .map((field) => ( - - ))} + + {team?.subscriptionId && ( + + )}
diff --git a/src/components/teams/form/settings/form-data.tsx b/src/components/teams/form/settings/form-data.tsx index 465361e..33c09e5 100644 --- a/src/components/teams/form/settings/form-data.tsx +++ b/src/components/teams/form/settings/form-data.tsx @@ -6,6 +6,8 @@ import { RegisterOptions } from 'react-hook-form'; export const FIELDS = { avatar: 'avatar', + avatarUpload: 'avatar-upload', + avatarRemove: 'avatar-remove', bio: 'bio', name: 'name', website: 'website', diff --git a/yarn.lock b/yarn.lock index 97e63ce..14ac36e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6859,6 +6859,13 @@ image-size@1.0.2: dependencies: queue "6.0.2" +image-size@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.1.1.tgz#ddd67d4dc340e52ac29ce5f546a09f4e29e840ac" + integrity sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ== + dependencies: + queue "6.0.2" + imagescript@^1.2.16: version "1.2.16" resolved "https://registry.yarnpkg.com/imagescript/-/imagescript-1.2.16.tgz#2272f535816bdcbaec9da4448de5c89a488756bd"