From 95995053f151f48e8d93ad38e333eaea3185d7a0 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Tue, 2 Apr 2024 23:26:40 +0200 Subject: [PATCH 01/26] fix: make image larger to avoid wrap --- src/components/dashboard/modpacks/modal/modpack-general.tsx | 2 +- src/components/dashboard/mods/modal/mods-general.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/dashboard/modpacks/modal/modpack-general.tsx b/src/components/dashboard/modpacks/modal/modpack-general.tsx index 5bd2aca..aa202d0 100644 --- a/src/components/dashboard/modpacks/modal/modpack-general.tsx +++ b/src/components/dashboard/modpacks/modal/modpack-general.tsx @@ -6,7 +6,7 @@ import { UseFormReturnType } from '@mantine/form'; import { ModpackModalFormValues } from './modpack-modal'; export function ModpackModalGeneral({ previewImg, modpack, form }: { form: UseFormReturnType, previewImg: string, modpack: Modpack | undefined }) { - const imageWidth = 210; + const imageWidth = 220; return ( diff --git a/src/components/dashboard/mods/modal/mods-general.tsx b/src/components/dashboard/mods/modal/mods-general.tsx index 878b4ea..eeb4b6b 100644 --- a/src/components/dashboard/mods/modal/mods-general.tsx +++ b/src/components/dashboard/mods/modal/mods-general.tsx @@ -11,7 +11,7 @@ export interface ModModalGeneralProps { } export function ModModalGeneral({ previewImg, mod, form }: ModModalGeneralProps) { - const imageWidth = 210; + const imageWidth = 220; return ( From 5927789634055c03648a690cffa50207ea37b6c5 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Tue, 2 Apr 2024 23:27:07 +0200 Subject: [PATCH 02/26] fix: remove useless `as unknown` --- src/server/data/modpacks-version.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/data/modpacks-version.ts b/src/server/data/modpacks-version.ts index ecbd414..9313a51 100644 --- a/src/server/data/modpacks-version.ts +++ b/src/server/data/modpacks-version.ts @@ -62,7 +62,7 @@ export async function removeModFromModpackVersion( export async function addModsToModpackVersion(id: string, data: FormData): Promise { await canAccess(); - const files = data.getAll('file') as unknown as File[]; + const files = data.getAll('file') as File[]; for (const file of files) { // Create all mod versions from the JAR file, can be multiple mods in one file const modVersions = await extractModVersionsFromJAR(file); From 9e7068dbe6d7391bdd87ce810b45a00072c30401 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Wed, 3 Apr 2024 01:21:39 +0200 Subject: [PATCH 03/26] feat #2 : started implementation of contribute page --- prisma/schema.prisma | 36 +++-- .../(pages)/(protected)/contribute/page.tsx | 150 ++++++++++++++++++ src/app/globals.scss | 13 +- src/components/navbar.tsx | 1 + .../submit/contribution-draft-item.tsx | 88 ++++++++++ src/components/submit/drafts/drafts-panel.tsx | 98 ++++++++++++ src/components/submit/submit.scss | 3 + src/server/data/contributions.ts | 47 ++++++ src/server/data/texture.ts | 4 + src/server/data/user.ts | 13 ++ src/types/index.d.ts | 9 ++ 11 files changed, 447 insertions(+), 15 deletions(-) create mode 100644 src/app/(pages)/(protected)/contribute/page.tsx create mode 100644 src/components/submit/contribution-draft-item.tsx create mode 100644 src/components/submit/drafts/drafts-panel.tsx create mode 100644 src/components/submit/submit.scss create mode 100644 src/server/data/contributions.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 04bac75..a312733 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,6 +20,7 @@ enum Resolution { } enum Status { + DRAFT PENDING ACCEPTED REJECTED @@ -183,14 +184,19 @@ model LinkedTexture { // } model Contribution { - id String @id @default(cuid()) - file String - date DateTime @default(now()) - users User[] + id String @id @default(cuid()) + file String + filename String + date DateTime @default(now()) + + ownerId String + owner User @relation(fields: [ownerId], references: [id]) + coAuthors User[] @relation("users") + resolution Resolution - status Status + status Status @default(DRAFT) - pollId String + pollId String @unique poll Poll @relation(fields: [pollId], references: [id]) Texture Texture? @relation(fields: [textureId], references: [id]) @@ -209,14 +215,16 @@ model Contribution { } model User { - id String @id @default(cuid()) + id String @id @default(cuid()) name String? - email String? @unique - emailVerified DateTime? @map("email_verified") + email String? @unique + emailVerified DateTime? @map("email_verified") image String? - role UserRole @default(USER) + role UserRole @default(USER) accounts Account[] - contributions Contribution[] + + ownedContributions Contribution[] + contributions Contribution[] @relation("users") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -232,9 +240,9 @@ model Poll { upvotes User[] @relation("Upvotes") downvotes User[] @relation("Downvotes") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - Contribution Contribution[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + Contribution Contribution? @@map("users_polls") } diff --git a/src/app/(pages)/(protected)/contribute/page.tsx b/src/app/(pages)/(protected)/contribute/page.tsx new file mode 100644 index 0000000..59a7d89 --- /dev/null +++ b/src/app/(pages)/(protected)/contribute/page.tsx @@ -0,0 +1,150 @@ +'use client'; + +import { Avatar, Badge, Card, Code, Group, MultiSelect, MultiSelectProps, Select, Stack, Text } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; +import { Resolution } from '@prisma/client'; +import { useState, useTransition } from 'react'; + +import { ContributionDraftPanel } from '~/components/submit/drafts/drafts-panel'; +import { useCurrentUser } from '~/hooks/use-current-user'; +import { useDeviceSize } from '~/hooks/use-device-size'; +import { useEffectOnce } from '~/hooks/use-effect-once'; +import { BREAKPOINT_MOBILE_LARGE } from '~/lib/constants'; +import { notify } from '~/lib/utils'; +import { createRawContributions, getSubmittedContributions, getDraftContributions } from '~/server/data/contributions'; +import { getPublicUsers } from '~/server/data/user'; +import type { ContributionWithCoAuthors, PublicUser } from '~/types'; + +const ContributePage = () => { + const [isPending, startTransition] = useTransition(); + const [windowWidth, _] = useDeviceSize(); + + const [resolution, setResolution] = useState(Resolution.x32); + const [users, setUsers] = useState([]); + const [selectedCoAuthors, setSelectedCoAuthors] = useState([]); + + const [contributions, setContributions] = useState(); + const [draftContributions, setDraftContributions] = useState(); + + const user = useCurrentUser()!; // the user is guaranteed to be logged in (per the layout) + + useEffectOnce(() => { + getSubmittedContributions(user.id!) + .then(setContributions) + .catch((err) => { + console.error(err); + notify('Error', 'Failed to fetch contributions', 'red'); + }); + + getDraftContributions(user.id!) + .then(setDraftContributions) + .catch((err) => { + console.error(err); + notify('Error', 'Failed to fetch draft contributions', 'red'); + }); + + getPublicUsers() + .then(setUsers) + .catch((err) => { + console.error(err); + notify('Error', 'Failed to fetch users', 'red'); + }); + }) + + const renderMultiSelectOption: MultiSelectProps['renderOption'] = ({ option }) => { + const user = users.find((u) => u.id === option.value)!; + + return ( + + +
+ {option.label} + {option.disabled && That's you!} +
+
+ ); + }; + + const filesDrop = (files: File[]) => { + startTransition(async () => { + const data = new FormData(); + files.forEach((file) => data.append('files', file)); + + await createRawContributions(user.id!, selectedCoAuthors, resolution, data); + getDraftContributions(user.id!).then(setDraftContributions); + }); + }; + + return ( + + + + + New contribution(s) + + Please do not submit textures for unsupported mod. Ask the admins to add the mod first. + + + + + + Others Contributions + Existing contribution for the selected texture. + + + + + + + +
+ + + + + Draft contribution(s) + {draftContributions.length} + + These contributions are not yet submitted and only visible by you. + + {draftContributions.map((contribution, index) => + + )} + + + + + ) +} \ No newline at end of file diff --git a/src/components/submit/submit.scss b/src/components/submit/submit.scss new file mode 100644 index 0000000..959455b --- /dev/null +++ b/src/components/submit/submit.scss @@ -0,0 +1,3 @@ +.contribution-item { + width: calc((100% - ((var(--contribution-item-count) - 1) * var(--mantine-spacing-md))) / var(--contribution-item-count)); +} \ No newline at end of file diff --git a/src/server/data/contributions.ts b/src/server/data/contributions.ts new file mode 100644 index 0000000..4368479 --- /dev/null +++ b/src/server/data/contributions.ts @@ -0,0 +1,47 @@ +'use server'; + +import { Status, type Contribution, type Resolution } from '@prisma/client'; + +import { db } from '~/lib/db'; +import { ContributionWithCoAuthors } from '~/types'; + +import { upload } from '../actions/files'; + +export async function getSubmittedContributions(ownerId: string): Promise { + return await db.contribution.findMany({ + where: { ownerId, status: { not: Status.DRAFT } }, + include: { coAuthors: { select: { id: true, name: true, image: true } } }, + }); +} + +export async function createRawContributions(ownerId: string, coAuthors: string[], resolution: Resolution, data: FormData): Promise { + const files = data.getAll('files') as File[]; + + const contributions: Contribution[] = []; + for (const file of files) { + const filepath = await upload(file, `textures/contributions/${ownerId}/`) + + const poll = await db.poll.create({ data: {} }); + const contribution = await db.contribution.create({ + data: { + ownerId, + coAuthors: { connect: coAuthors.map((id) => ({ id })) }, + resolution, + file: filepath, + filename: file.name, + pollId: poll.id, + } + }) + + contributions.push(contribution); + } + + return contributions; +} + +export async function getDraftContributions(ownerId: string): Promise { + return await db.contribution.findMany({ + where: { ownerId, status: Status.DRAFT }, + include: { coAuthors: { select: { id: true, name: true, image: true } } }, + }); +} diff --git a/src/server/data/texture.ts b/src/server/data/texture.ts index 19f7e9a..1cbda30 100644 --- a/src/server/data/texture.ts +++ b/src/server/data/texture.ts @@ -7,6 +7,10 @@ import type{ Progression } from '~/types'; import { remove } from '../actions/files'; +export async function getTextures(): Promise { + return db.texture.findMany(); +} + export async function createTexture({ name, filepath, hash }: { name: string, filepath: string, hash: string }): Promise { return db.texture.create({ data: { diff --git a/src/server/data/user.ts b/src/server/data/user.ts index 6d5235a..18ac134 100644 --- a/src/server/data/user.ts +++ b/src/server/data/user.ts @@ -4,6 +4,7 @@ import { User, UserRole } from '@prisma/client'; import { canAccess } from '~/lib/auth'; import { db } from '~/lib/db'; +import type { PublicUser } from '~/types'; /** * Get all users from the database @@ -14,6 +15,18 @@ export async function getUsers(): Promise { return db.user.findMany(); } +export async function getPublicUsers(): Promise { + const users = await db.user.findMany({ where: { role: { not: UserRole.BANNED }}, orderBy: { name: 'asc' }}); + + return users + .filter((user) => user.name !== null) + .map((user) => ({ + id: user.id, + name: user.name ?? 'Unknown', // This should never happen (filtered above) + image: user.image ?? '/icon.png', + })); +} + /** * Update a user's role * @param {string} id the user to update diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 1554f20..c4730af 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,4 +1,5 @@ import type { + Contribution, LinkedTexture, Mod, Modpack, @@ -16,6 +17,14 @@ export type Prettify = { export type ModpackVersionWithMods = ModpackVersion & { mods: ModVersion[] }; export type ModVersionWithModpacks = ModVersion & { modpacks: Modpack[] }; +export type PublicUser = { + id: string; + name: string | null; + image: string | null; +}; + +export type ContributionWithCoAuthors = Contribution & { coAuthors: PublicUser[] }; + export type Progression = { linkedTextures: number; textures: { From 71a60498568062443e154d3266549f5eb8c11f28 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Wed, 3 Apr 2024 18:16:38 +0200 Subject: [PATCH 04/26] feat #2 : add contribution deletion --- .../submit/contribution-draft-item.tsx | 49 +++++++++++++------ src/components/submit/drafts/drafts-panel.tsx | 14 ++++-- src/server/data/contributions.ts | 4 ++ 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/components/submit/contribution-draft-item.tsx b/src/components/submit/contribution-draft-item.tsx index be47879..97a54a4 100644 --- a/src/components/submit/contribution-draft-item.tsx +++ b/src/components/submit/contribution-draft-item.tsx @@ -1,20 +1,36 @@ import { Button, Card, Group, Image, Stack, Text } from '@mantine/core'; import { Texture } from '@prisma/client'; -import { useState } from 'react'; +import { useState, useTransition } from 'react'; import { FaEdit, FaFileAlt } from 'react-icons/fa'; import { useDeviceSize } from '~/hooks/use-device-size'; import { BREAKPOINT_MOBILE_LARGE, BREAKPOINT_DESKTOP_MEDIUM, BREAKPOINT_DESKTOP_LARGE } from '~/lib/constants'; import { gradient, gradientDanger } from '~/lib/utils'; -import { ContributionWithCoAuthors } from '~/types'; +import { deleteContribution } from '~/server/data/contributions'; +import type { ContributionWithCoAuthors } from '~/types'; import './submit.scss'; -export function ContributionDraftItem({ contribution, openModal }: { contribution: ContributionWithCoAuthors, openModal: (c: ContributionWithCoAuthors) => void }) { - const [_contribution, setContribution] = useState(contribution); +export interface ContributionDraftItemProps { + contribution: ContributionWithCoAuthors; + openModal: (c: ContributionWithCoAuthors) => void; + onDelete: () => void; +} + +export function ContributionDraftItem({ contribution, openModal, onDelete }: ContributionDraftItemProps) { + const [isPending, startTransition] = useTransition(); const [windowWidth, _] = useDeviceSize(); + const deleteFn = () => { + if (contribution === undefined) return; + + startTransition(() => { + deleteContribution(contribution.id); + onDelete(); + }) + } + return ( openModal(_contribution)} + onClick={() => openModal(contribution)} color={gradient.to} style={{ position: 'absolute', top: 'var(--mantine-spacing-md)', right: 'var(--mantine-spacing-md)' }} > - {_contribution.filename.endsWith('.png') && + {contribution.filename.endsWith('.png') && } { - (_contribution.filename.endsWith('.json') || _contribution.filename.endsWith('.mcmeta')) && + (contribution.filename.endsWith('.json') || contribution.filename.endsWith('.mcmeta')) && } - {_contribution.filename} - Resolution : {_contribution.resolution} - Creation : {_contribution.createdAt.toLocaleString()} - Co-authors : {_contribution.coAuthors.length === 0 ? 'None' : _contribution.coAuthors.map((ca) => ca.name).join(', ')} + {contribution.filename} + Resolution : {contribution.resolution} + Creation : {contribution.createdAt.toLocaleString()} + Co-authors : {contribution.coAuthors.length === 0 ? 'None' : contribution.coAuthors.map((ca) => ca.name).join(', ')} diff --git a/src/components/submit/drafts/drafts-panel.tsx b/src/components/submit/drafts/drafts-panel.tsx index 90dc562..388e139 100644 --- a/src/components/submit/drafts/drafts-panel.tsx +++ b/src/components/submit/drafts/drafts-panel.tsx @@ -15,6 +15,7 @@ import { ContributionDraftItem } from '../contribution-draft-item'; export function ContributionDraftPanel({ draftContributions }: { draftContributions: ContributionWithCoAuthors[] }) { const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [modalContribution, setModalContribution] = useState(null); + const [contributions, setContributions] = useState(draftContributions); const [textures, setTextures] = useState([]); const modalImageWidth = 360; @@ -83,12 +84,19 @@ export function ContributionDraftPanel({ draftContributions }: { draftContributi Draft contribution(s) - {draftContributions.length} + {contributions.length} These contributions are not yet submitted and only visible by you. - {draftContributions.map((contribution, index) => - + {contributions.map((contribution, index) => + { + setContributions(contributions.filter((c) => c.id !== contribution.id)); + }} + openModal={openModalWithContribution} + /> )} diff --git a/src/server/data/contributions.ts b/src/server/data/contributions.ts index 4368479..c40868e 100644 --- a/src/server/data/contributions.ts +++ b/src/server/data/contributions.ts @@ -45,3 +45,7 @@ export async function getDraftContributions(ownerId: string): Promise { + return await db.contribution.delete({ where: { id } }); +} From 24f98c6ff6177d5c25417fe5dfae038ccbafb141 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Wed, 3 Apr 2024 18:21:10 +0200 Subject: [PATCH 05/26] feat #2 : move draft modal into its own component --- src/components/submit/drafts/drafts-modal.tsx | 55 +++++++++++++++++++ src/components/submit/drafts/drafts-panel.tsx | 41 +------------- 2 files changed, 57 insertions(+), 39 deletions(-) create mode 100644 src/components/submit/drafts/drafts-modal.tsx diff --git a/src/components/submit/drafts/drafts-modal.tsx b/src/components/submit/drafts/drafts-modal.tsx new file mode 100644 index 0000000..a3443d6 --- /dev/null +++ b/src/components/submit/drafts/drafts-modal.tsx @@ -0,0 +1,55 @@ +import type { Texture } from '@prisma/client'; + +import { Button, Group, Image, Select, Skeleton, Stack, Text, TextInput } from '@mantine/core'; +import { FaChevronLeft, FaChevronRight } from 'react-icons/fa'; + +import type { ContributionWithCoAuthors } from '~/types'; + +export interface ContributionDraftModalProps { + contribution: ContributionWithCoAuthors; + textures: Texture[]; +} + +export function ContributionDraftModal({ contribution, textures }: ContributionDraftModalProps) { + const modalImageWidth = 360; + + return ( + + + + Your Contribution + This is the contribution you are currently editing. + + + + + + + Default Texture + This is the texture that will be contributed to. + + + - - - - Others Contributions - Existing contribution for the selected texture. - - - - - - - - + {modalContribution && } From 49a95463b0921db4147a8ad364c5a1f3e9ac8b75 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Wed, 3 Apr 2024 18:28:09 +0200 Subject: [PATCH 06/26] feat #2 : make sure contribution file is also deleted --- src/server/data/contributions.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/server/data/contributions.ts b/src/server/data/contributions.ts index c40868e..c7d3d7e 100644 --- a/src/server/data/contributions.ts +++ b/src/server/data/contributions.ts @@ -5,7 +5,7 @@ import { Status, type Contribution, type Resolution } from '@prisma/client'; import { db } from '~/lib/db'; import { ContributionWithCoAuthors } from '~/types'; -import { upload } from '../actions/files'; +import { remove, upload } from '../actions/files'; export async function getSubmittedContributions(ownerId: string): Promise { return await db.contribution.findMany({ @@ -47,5 +47,8 @@ export async function getDraftContributions(ownerId: string): Promise { + const contributionFile = await db.contribution.findUnique({ where: { id } }).then((c) => c?.file); + if (contributionFile) await remove(contributionFile as `files/${string}`); + return await db.contribution.delete({ where: { id } }); } From cfae71071e8cbdf28c6e02f73cbfe2d0fc1a97fe Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Fri, 5 Apr 2024 01:23:29 +0200 Subject: [PATCH 07/26] feat #2 : finish basic modal setup for draft contributions --- .../(pages)/(protected)/contribute/page.tsx | 50 +-- src/app/globals.scss | 14 +- src/components/submit/co-authors-select.tsx | 61 ++++ src/components/submit/drafts/drafts-modal.tsx | 311 +++++++++++++++--- src/components/submit/drafts/drafts-panel.tsx | 27 +- src/server/data/contributions.ts | 47 ++- src/types/index.d.ts | 2 + 7 files changed, 420 insertions(+), 92 deletions(-) create mode 100644 src/components/submit/co-authors-select.tsx diff --git a/src/app/(pages)/(protected)/contribute/page.tsx b/src/app/(pages)/(protected)/contribute/page.tsx index 59a7d89..1985a88 100644 --- a/src/app/(pages)/(protected)/contribute/page.tsx +++ b/src/app/(pages)/(protected)/contribute/page.tsx @@ -1,10 +1,11 @@ 'use client'; -import { Avatar, Badge, Card, Code, Group, MultiSelect, MultiSelectProps, Select, Stack, Text } from '@mantine/core'; +import { Badge, Card, Code, Group, Select, Stack, Text } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { Resolution } from '@prisma/client'; import { useState, useTransition } from 'react'; +import { CoAuthorsSelector } from '~/components/submit/co-authors-select'; import { ContributionDraftPanel } from '~/components/submit/drafts/drafts-panel'; import { useCurrentUser } from '~/hooks/use-current-user'; import { useDeviceSize } from '~/hooks/use-device-size'; @@ -12,7 +13,6 @@ import { useEffectOnce } from '~/hooks/use-effect-once'; import { BREAKPOINT_MOBILE_LARGE } from '~/lib/constants'; import { notify } from '~/lib/utils'; import { createRawContributions, getSubmittedContributions, getDraftContributions } from '~/server/data/contributions'; -import { getPublicUsers } from '~/server/data/user'; import type { ContributionWithCoAuthors, PublicUser } from '~/types'; const ContributePage = () => { @@ -20,8 +20,7 @@ const ContributePage = () => { const [windowWidth, _] = useDeviceSize(); const [resolution, setResolution] = useState(Resolution.x32); - const [users, setUsers] = useState([]); - const [selectedCoAuthors, setSelectedCoAuthors] = useState([]); + const [selectedCoAuthors, setSelectedCoAuthors] = useState([]); const [contributions, setContributions] = useState(); const [draftContributions, setDraftContributions] = useState(); @@ -42,35 +41,14 @@ const ContributePage = () => { console.error(err); notify('Error', 'Failed to fetch draft contributions', 'red'); }); - - getPublicUsers() - .then(setUsers) - .catch((err) => { - console.error(err); - notify('Error', 'Failed to fetch users', 'red'); - }); - }) - - const renderMultiSelectOption: MultiSelectProps['renderOption'] = ({ option }) => { - const user = users.find((u) => u.id === option.value)!; - - return ( - - -
- {option.label} - {option.disabled && That's you!} -
-
- ); - }; + }); const filesDrop = (files: File[]) => { startTransition(async () => { const data = new FormData(); files.forEach((file) => data.append('files', file)); - await createRawContributions(user.id!, selectedCoAuthors, resolution, data); + await createRawContributions(user.id!, selectedCoAuthors.map((u) => u.id), resolution, data); getDraftContributions(user.id!).then(setDraftContributions); }); }; @@ -93,17 +71,13 @@ const ContributePage = () => { style={windowWidth <= BREAKPOINT_MOBILE_LARGE ? { width: '100%' } : { width: 'calc((100% - var(--mantine-spacing-md)) * .2)' }} required /> - ({ value: u.id, label: u.name ?? 'Unknown', disabled: u.id === user.id }))} - renderOption={renderMultiSelectOption} - defaultValue={[]} - onChange={setSelectedCoAuthors} - style={windowWidth <= BREAKPOINT_MOBILE_LARGE ? { width: '100%' } : { width: 'calc((100% - var(--mantine-spacing-md)) * .8)' }} - hidePickedOptions - searchable - clearable +
diff --git a/src/app/globals.scss b/src/app/globals.scss index 068c253..48bea82 100644 --- a/src/app/globals.scss +++ b/src/app/globals.scss @@ -22,14 +22,20 @@ main { @apply h-full; } -.image-background { - background-color: var(--mantine-color-dark-7); -} - .image-pixelated { image-rendering: pixelated; } +.texture-background { + background-color: var(--mantine-color-dark-6); +} +:where([data-mantine-color-scheme='light']) .texture-background { + background-color: var(--mantine-color-gray-1); +} + +.image-background { + background-color: var(--mantine-color-dark-7); +} :where([data-mantine-color-scheme='light']) .image-background { background-color: var(--mantine-primary-color-1); } diff --git a/src/components/submit/co-authors-select.tsx b/src/components/submit/co-authors-select.tsx new file mode 100644 index 0000000..51803ad --- /dev/null +++ b/src/components/submit/co-authors-select.tsx @@ -0,0 +1,61 @@ +import { Avatar, Group, MultiSelect, type MultiSelectProps, Text } from '@mantine/core'; +import { useState } from 'react'; + +import type { useCurrentUser } from '~/hooks/use-current-user'; +import { useEffectOnce } from '~/hooks/use-effect-once'; +import { notify } from '~/lib/utils'; +import { getPublicUsers } from '~/server/data/user'; +import { PublicUser } from '~/types'; + +/** + * Select co-authors for a contribution. + * The current user is excluded from the list of selectable co-authors. + */ +export interface CoAuthorsSelectorProps extends MultiSelectProps { + author: NonNullable>; + onCoAuthorsSelect: (coAuthors: PublicUser[]) => void; +} + +export function CoAuthorsSelector(props: CoAuthorsSelectorProps) { + const { author, onCoAuthorsSelect } = props; + const [users, setUsers] = useState([]); + + useEffectOnce(() => { + getPublicUsers() + .then(setUsers) + .catch((err) => { + console.error(err); + notify('Error', 'Failed to fetch users', 'red'); + }); + }); + + const renderMultiSelectOption: MultiSelectProps['renderOption'] = ({ option }) => { + const user = users.find((u) => u.id === option.value)!; + + return ( + + +
+ {option.label} + {option.disabled && That's you!} +
+
+ ); + }; + + return ( + ({ value: u.id, label: u.name ?? 'Unknown', disabled: u.id === author.id }))} + renderOption={renderMultiSelectOption} + defaultValue={[]} + onChange={(userIds: string[]) => onCoAuthorsSelect(userIds.map((u) => users.find((user) => user.id === u)!))} + hidePickedOptions + searchable + clearable + {...props} + /> + ); +} \ No newline at end of file diff --git a/src/components/submit/drafts/drafts-modal.tsx b/src/components/submit/drafts/drafts-modal.tsx index a3443d6..fda1430 100644 --- a/src/components/submit/drafts/drafts-modal.tsx +++ b/src/components/submit/drafts/drafts-modal.tsx @@ -1,55 +1,286 @@ -import type { Texture } from '@prisma/client'; - -import { Button, Group, Image, Select, Skeleton, Stack, Text, TextInput } from '@mantine/core'; +import { Avatar, Button, Card, Divider, Group, Image, MultiSelectProps, Select, Stack, Text, Title } from '@mantine/core'; +import { Resolution, type Texture } from '@prisma/client'; +import { useState, useTransition } from 'react'; import { FaChevronLeft, FaChevronRight } from 'react-icons/fa'; +import { PiMagicWandBold } from 'react-icons/pi'; + +import { useCurrentUser } from '~/hooks/use-current-user'; +import { useDeviceSize } from '~/hooks/use-device-size'; +import { useEffectOnce } from '~/hooks/use-effect-once'; +import { BREAKPOINT_MOBILE_LARGE } from '~/lib/constants'; +import { gradient, gradientDanger } from '~/lib/utils'; +import { getContributionsOfTexture, updateDraftContribution } from '~/server/data/contributions'; +import type { ContributionWithCoAuthors, ContributionWithCoAuthorsAndPoll, PublicUser } from '~/types'; -import type { ContributionWithCoAuthors } from '~/types'; +import { CoAuthorsSelector } from '../co-authors-select'; export interface ContributionDraftModalProps { contribution: ContributionWithCoAuthors; textures: Texture[]; + onClose: (editedContribution: ContributionWithCoAuthors) => void; } -export function ContributionDraftModal({ contribution, textures }: ContributionDraftModalProps) { - const modalImageWidth = 360; +export const IMAGE_WIDTH = 320; +export const MODAL_WIDTH = 3.1 * IMAGE_WIDTH; +export const ROW_HEIGHT = 36; + +export function ContributionDraftModal({ contribution, textures, onClose }: ContributionDraftModalProps) { + const [isPending, startTransition] = useTransition(); + const [selectedTexture, setSelectedTexture] = useState(null); + const [selectedCoAuthors, setSelectedCoAuthors] = useState(contribution.coAuthors); + const [selectedResolution, setSelectedResolution] = useState(contribution.resolution); + const [windowWidth, _] = useDeviceSize(); + + const [selectedTextureContributions, setSelectedTextureContributions] = useState([]); + const [selectedTextureContributionsIndex, setSelectedTextureContributionsIndex] = useState(0); + const [displayedSelectedTextureContributions, setDisplayedSelectedTextureContributions] = useState(); + + const author = useCurrentUser()!; + + useEffectOnce(() => { + if (contribution.textureId) selectedTextureUpdated(contribution.textureId); + }); + + const selectedTextureUpdated = (textureId: string | null) => { + if (textureId === null) { + setSelectedTexture(null); + setSelectedTextureContributions([]); + setSelectedTextureContributionsIndex(0); + setDisplayedSelectedTextureContributions(undefined); + return; + } + + const texture = textures.find((t) => t.id === textureId)!; + getContributionsOfTexture(textureId) + .then((res) => { + setSelectedTexture(texture); + setSelectedTextureContributions(res); + setSelectedTextureContributionsIndex(0); + setDisplayedSelectedTextureContributions(res[0]); + }) + .catch(console.error) + } + + const renderMultiSelectOption: MultiSelectProps['renderOption'] = ({ option }) => { + const texture = textures.find((u) => u.id === option.value)!; + + return ( + + +
+ {sanitizeTextureName(texture.name)} + {option.disabled && Already selected!} + {/* TODO for #18 {!option.disabled && {texture.aliases.join(', ')}} */} +
+
+ ); + }; + + /** + * @deprecated To be removed when #18 is implemented. + */ + const sanitizeTextureName = (name: string): string => { + return name.split('_')[1]?.split('.')[0]; + } + + const previousContribution = () => { + if (selectedTextureContributionsIndex === 0) return; + let index = selectedTextureContributionsIndex - 1; + setSelectedTextureContributionsIndex(index); + setDisplayedSelectedTextureContributions(selectedTextureContributions[index]); + } + + const nextContribution = () => { + if (selectedTextureContributionsIndex === selectedTextureContributions.length - 1) return; + let index = selectedTextureContributionsIndex + 1; + setSelectedTextureContributionsIndex(index); + setDisplayedSelectedTextureContributions(selectedTextureContributions[index]); + } + + const updateDraft = () => { + if (!selectedTexture) return; + + startTransition(() => { + updateDraftContribution({ + ownerId: author.id!, + contributionId: contribution.id, + coAuthors: selectedCoAuthors.map((c) => c.id), + resolution: selectedResolution, + textureId: selectedTexture.id, + }) + .then(onClose) + .catch(console.error) + }) + } + + const cancelAndClose = () => { + onClose(contribution); + } return ( - - - - Your Contribution - This is the contribution you are currently editing. + + + {/* User contribution */} + + + Your Contribution + This is the contribution you are currently editing. + + + + {contribution.filename} + + + - - - - - - Default Texture - This is the texture that will be contributed to. + {/* Default texture */} + + + Default Texture + This is the texture that will be contributed to. + + + + - - - - Others Contributions - Existing contribution for the selected texture. + {/* Existing contribution */} + + + Existing Contributions + Contributions of the selected texture. + + + + {selectedTextureContributions.length > 0 && + + {selectedTextureContributionsIndex + 1} / {selectedTextureContributions.length} + + } + {selectedTextureContributions.length === 0 && + + - / - + + } + + + {!selectedTexture && selectedTextureContributions.length === 0 && + + + Select a texture first! + + + } + {selectedTexture && selectedTextureContributions.length === 0 && + + + No contributions for this texture. + + + } + {selectedTexture && selectedTextureContributions.length > 0 && displayedSelectedTextureContributions && + + } - - - - - - - + +
+ + Contribution Info + + + { - {draftContributions && draftContributions.length > 0 && } - - - - Submitted contribution(s) - {contributions?.length ?? '?'} - - - {!contributions && Loading...} - {contributions && contributions.length === 0 && Nothing yet} - {contributions && contributions.length > 0 &&

TODO

} -
-
+ {draftContributions && + c.id).join('')} + onUpdate={reloadSubmitted} + /> + } + {contributions && + c.id).join('')} + /> + } ) } diff --git a/src/components/submit/co-authors-select.tsx b/src/components/submit/co-authors-select.tsx index 51803ad..b5daa0d 100644 --- a/src/components/submit/co-authors-select.tsx +++ b/src/components/submit/co-authors-select.tsx @@ -16,8 +16,7 @@ export interface CoAuthorsSelectorProps extends MultiSelectProps { onCoAuthorsSelect: (coAuthors: PublicUser[]) => void; } -export function CoAuthorsSelector(props: CoAuthorsSelectorProps) { - const { author, onCoAuthorsSelect } = props; +export function CoAuthorsSelector({ author, onCoAuthorsSelect, ...props}: CoAuthorsSelectorProps) { const [users, setUsers] = useState([]); useEffectOnce(() => { diff --git a/src/components/submit/contribution-draft-item.tsx b/src/components/submit/drafts/drafts-item.tsx similarity index 54% rename from src/components/submit/contribution-draft-item.tsx rename to src/components/submit/drafts/drafts-item.tsx index 97a54a4..796f302 100644 --- a/src/components/submit/contribution-draft-item.tsx +++ b/src/components/submit/drafts/drafts-item.tsx @@ -1,16 +1,16 @@ import { Button, Card, Group, Image, Stack, Text } from '@mantine/core'; -import { Texture } from '@prisma/client'; -import { useState, useTransition } from 'react'; +import { useTransition } from 'react'; import { FaEdit, FaFileAlt } from 'react-icons/fa'; +import { useCurrentUser } from '~/hooks/use-current-user'; import { useDeviceSize } from '~/hooks/use-device-size'; -import { BREAKPOINT_MOBILE_LARGE, BREAKPOINT_DESKTOP_MEDIUM, BREAKPOINT_DESKTOP_LARGE } from '~/lib/constants'; -import { gradient, gradientDanger } from '~/lib/utils'; -import { deleteContribution } from '~/server/data/contributions'; +import { BREAKPOINT_MOBILE_LARGE, BREAKPOINT_DESKTOP_MEDIUM, BREAKPOINT_TABLET } from '~/lib/constants'; +import { cn, gradient, gradientDanger } from '~/lib/utils'; +import { deleteContributions, submitContribution } from '~/server/data/contributions'; import type { ContributionWithCoAuthors } from '~/types'; -import './submit.scss'; +import '../submit.scss'; export interface ContributionDraftItemProps { contribution: ContributionWithCoAuthors; @@ -22,11 +22,21 @@ export function ContributionDraftItem({ contribution, openModal, onDelete }: Con const [isPending, startTransition] = useTransition(); const [windowWidth, _] = useDeviceSize(); + const imgWidth = windowWidth <= BREAKPOINT_MOBILE_LARGE ? 60 : 90; + const author = useCurrentUser()!; + const deleteFn = () => { if (contribution === undefined) return; startTransition(() => { - deleteContribution(contribution.id); + deleteContributions(author.id!, contribution.id); + onDelete(); + }) + } + + const submit = () => { + startTransition(() => { + submitContribution(author.id!, contribution.id); onDelete(); }) } @@ -42,62 +52,77 @@ export function ContributionDraftItem({ contribution, openModal, onDelete }: Con ? 1 : windowWidth <= BREAKPOINT_DESKTOP_MEDIUM ? 2 - : windowWidth <= BREAKPOINT_DESKTOP_LARGE - ? 3 - : 4 + : 3 }} > - - + {windowWidth > BREAKPOINT_TABLET && + + } + {contribution.filename.endsWith('.png') && } { (contribution.filename.endsWith('.json') || contribution.filename.endsWith('.mcmeta')) && } - + {contribution.filename} Resolution : {contribution.resolution} Creation : {contribution.createdAt.toLocaleString()} Co-authors : {contribution.coAuthors.length === 0 ? 'None' : contribution.coAuthors.map((ca) => ca.name).join(', ')} - + + {windowWidth <= BREAKPOINT_TABLET && + + } diff --git a/src/components/submit/drafts/drafts-modal.tsx b/src/components/submit/drafts/drafts-modal.tsx index fda1430..012b088 100644 --- a/src/components/submit/drafts/drafts-modal.tsx +++ b/src/components/submit/drafts/drafts-modal.tsx @@ -1,4 +1,4 @@ -import { Avatar, Button, Card, Divider, Group, Image, MultiSelectProps, Select, Stack, Text, Title } from '@mantine/core'; +import { Avatar, Button, Card, Container, Group, Image, MultiSelectProps, Select, Stack, Text, Title } from '@mantine/core'; import { Resolution, type Texture } from '@prisma/client'; import { useState, useTransition } from 'react'; import { FaChevronLeft, FaChevronRight } from 'react-icons/fa'; @@ -7,7 +7,7 @@ import { PiMagicWandBold } from 'react-icons/pi'; import { useCurrentUser } from '~/hooks/use-current-user'; import { useDeviceSize } from '~/hooks/use-device-size'; import { useEffectOnce } from '~/hooks/use-effect-once'; -import { BREAKPOINT_MOBILE_LARGE } from '~/lib/constants'; +import { BREAKPOINT_DESKTOP_MEDIUM, BREAKPOINT_MOBILE_LARGE, BREAKPOINT_TABLET } from '~/lib/constants'; import { gradient, gradientDanger } from '~/lib/utils'; import { getContributionsOfTexture, updateDraftContribution } from '~/server/data/contributions'; import type { ContributionWithCoAuthors, ContributionWithCoAuthorsAndPoll, PublicUser } from '~/types'; @@ -20,10 +20,6 @@ export interface ContributionDraftModalProps { onClose: (editedContribution: ContributionWithCoAuthors) => void; } -export const IMAGE_WIDTH = 320; -export const MODAL_WIDTH = 3.1 * IMAGE_WIDTH; -export const ROW_HEIGHT = 36; - export function ContributionDraftModal({ contribution, textures, onClose }: ContributionDraftModalProps) { const [isPending, startTransition] = useTransition(); const [selectedTexture, setSelectedTexture] = useState(null); @@ -31,6 +27,13 @@ export function ContributionDraftModal({ contribution, textures, onClose }: Cont const [selectedResolution, setSelectedResolution] = useState(contribution.resolution); const [windowWidth, _] = useDeviceSize(); + const rowHeight = 36; + const colWidth = windowWidth <= BREAKPOINT_MOBILE_LARGE ? '100%' : 'calc((100% - (2 * var(--mantine-spacing-md))) / 3)' as const; + + const columnStyle = { + width: colWidth, + }; + const [selectedTextureContributions, setSelectedTextureContributions] = useState([]); const [selectedTextureContributionsIndex, setSelectedTextureContributionsIndex] = useState(0); const [displayedSelectedTextureContributions, setDisplayedSelectedTextureContributions] = useState(); @@ -118,50 +121,45 @@ export function ContributionDraftModal({ contribution, textures, onClose }: Cont } return ( - - + + {/* User contribution */} - - + + Your Contribution - This is the contribution you are currently editing. + The file you're submitting. - - + + {contribution.filename} {/* Default texture */} - - + + Default Texture - This is the texture that will be contributed to. + The targeted texture. - + + + + + + + + + {displayedContributions.length} + }> + Submitted + + + + + + searchContribution(form.values.search)} + {...form.getInputProps('search')} + className="contribution-item" + style={{ + '--contribution-item-count': windowWidth <= BREAKPOINT_MOBILE_LARGE + ? .9 + : windowWidth <= BREAKPOINT_DESKTOP_MEDIUM + ? 1.9 + : 2.9 + }} + /> + { + setDeletionMode(event.currentTarget.checked); + if (!event.currentTarget.checked) setDeletionList([]); + }} + /> + + + + + {displayedContributions && displayedContributions.length === 0 && Nothing yet} + {displayedContributions && displayedContributions.length > 0 && + <> + + {searchedContributions.length > 0 && searchedContributions.map((contribution, index) => + checkDeletionListFor(contribution.id)} + /> + )} + {searchedContributions.length === 0 && + No results found + } + + + } + + + + + + + ) +} \ No newline at end of file diff --git a/src/server/data/contributions.ts b/src/server/data/contributions.ts index 4d75332..be88c71 100644 --- a/src/server/data/contributions.ts +++ b/src/server/data/contributions.ts @@ -8,12 +8,12 @@ import { ContributionWithCoAuthors, ContributionWithCoAuthorsAndPoll } from '~/t import { remove, upload } from '../actions/files'; -export async function getSubmittedContributions(ownerId: string): Promise { +export async function getSubmittedContributions(ownerId: string): Promise { await canAccess(UserRole.ADMIN, ownerId); return await db.contribution.findMany({ where: { ownerId, status: { not: Status.DRAFT } }, - include: { coAuthors: { select: { id: true, name: true, image: true } } }, + include: { coAuthors: { select: { id: true, name: true, image: true } }, poll: true }, }); } @@ -87,9 +87,21 @@ export async function updateDraftContribution({ }); } -export async function deleteContribution(id: string): Promise { - const contributionFile = await db.contribution.findUnique({ where: { id } }).then((c) => c?.file); - if (contributionFile) await remove(contributionFile as `files/${string}`); +export async function submitContribution(ownerId: string, id: string) { + await canAccess(UserRole.ADMIN, ownerId); + + return await db.contribution.update({ + where: { id }, + data: { status: Status.PENDING }, + }); +} + +export async function deleteContributions(ownerId: string, ...ids: string[]): Promise { + await canAccess(UserRole.ADMIN, ownerId); + + const contributions = await db.contribution.findMany({ where: { id: { in: ids } } }); + const contributionFiles = contributions.map((c) => c.file); - return await db.contribution.delete({ where: { id } }); + await Promise.all(contributionFiles.map((file) => remove(file as `files/${string}`))); + await db.contribution.deleteMany({ where: { id: { in: ids } } }); } From 2330dc1706926c69a6c222a72cb8cfcc0a351a2f Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Sun, 7 Apr 2024 14:07:19 +0200 Subject: [PATCH 11/26] feat #2 : add poll counts to accepted submissions --- .vscode/settings.json | 4 +- .../submit/submitted/submitted-item.tsx | 50 +++++++++++++------ src/server/data/polls.ts | 12 +++++ src/types/index.d.ts | 5 ++ 4 files changed, 56 insertions(+), 15 deletions(-) create mode 100644 src/server/data/polls.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 4cce1b9..2fe8600 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cSpell.words": [ "dependants", + "downvotes", "dropzone", "hookform", "mantine", @@ -11,7 +12,8 @@ "prisma", "stylelint", "tailwindcss", - "unzipper" + "unzipper", + "upvotes" ], "files.exclude": { "**/.git": true, diff --git a/src/components/submit/submitted/submitted-item.tsx b/src/components/submit/submitted/submitted-item.tsx index 3588807..cb3fa97 100644 --- a/src/components/submit/submitted/submitted-item.tsx +++ b/src/components/submit/submitted/submitted-item.tsx @@ -1,12 +1,15 @@ import { Badge, Card, Group, Image, Stack, Text } from '@mantine/core'; -import { Status } from '@prisma/client'; +import { Poll, Status } from '@prisma/client'; import { ClassValue } from 'clsx'; -import { FaFileAlt } from 'react-icons/fa'; +import { useState } from 'react'; +import { FaArrowUp, FaArrowDown, FaFileAlt } from 'react-icons/fa'; import { useDeviceSize } from '~/hooks/use-device-size'; +import { useEffectOnce } from '~/hooks/use-effect-once'; import { BREAKPOINT_DESKTOP_MEDIUM, BREAKPOINT_MOBILE_LARGE, BREAKPOINT_TABLET } from '~/lib/constants'; import { cn } from '~/lib/utils'; -import { ContributionWithCoAuthorsAndPoll } from '~/types'; +import { getPollResult } from '~/server/data/polls'; +import { ContributionWithCoAuthorsAndPoll, PollResults } from '~/types'; export interface ContributionSubmittedItemProps { contribution: ContributionWithCoAuthorsAndPoll; @@ -17,6 +20,11 @@ export interface ContributionSubmittedItemProps { export function ContributionSubmittedItem({ contribution, className, onClick }: ContributionSubmittedItemProps) { const [windowWidth, _] = useDeviceSize(); const imgWidth = windowWidth <= BREAKPOINT_MOBILE_LARGE ? 60 : 90; + const [poll, setPoll] = useState(); + + useEffectOnce(() => { + getPollResult(contribution.pollId).then((res) => setPoll(res)); + }) return ( Co-authors : {contribution.coAuthors.length === 0 ? 'None' : contribution.coAuthors.map((ca) => ca.name).join(', ')} - + + {windowWidth <= BREAKPOINT_TABLET ? contribution.status.slice(0, 1) : contribution.status} + + {poll && contribution.status === Status.ACCEPTED && + + + {poll.upvotes} + + + + {poll.downvotes} + + + } - variant="filled" - > - {windowWidth <= BREAKPOINT_TABLET ? contribution.status.slice(0, 1) : contribution.status} - + ) diff --git a/src/server/data/polls.ts b/src/server/data/polls.ts new file mode 100644 index 0000000..b1f7172 --- /dev/null +++ b/src/server/data/polls.ts @@ -0,0 +1,12 @@ +'use server'; + +import { db } from '~/lib/db'; +import type { PollResults } from '~/types'; + +export async function getPollResult(id: string): Promise { + const res = await db.poll.findFirstOrThrow({ where: { id }, include: { upvotes: true, downvotes: true } }); + return { + upvotes: res.upvotes.length, + downvotes: res.downvotes.length, + }; +} \ No newline at end of file diff --git a/src/types/index.d.ts b/src/types/index.d.ts index b6245d7..edde67c 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -37,6 +37,11 @@ export type Progression = { }; } +export interface PollResults { + upvotes: number; + downvotes: number; +} + export type ResourceWithProgression = Prettify; export type ModVersionWithProgression = Prettify Date: Sun, 7 Apr 2024 14:07:45 +0200 Subject: [PATCH 12/26] feat #2 : fix deleted submissions still being displayed --- src/components/submit/submitted/submitted-panel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/submit/submitted/submitted-panel.tsx b/src/components/submit/submitted/submitted-panel.tsx index e766083..cf49d74 100644 --- a/src/components/submit/submitted/submitted-panel.tsx +++ b/src/components/submit/submitted/submitted-panel.tsx @@ -58,6 +58,7 @@ export function ContributionSubmittedPanel({ contributions }: ContributionDraftP closeModal(); setDisplayedContributions(displayedContributions.filter((c) => !deletionList.includes(c.id))); setDeletionList([]); + searchContribution(form.values.search); // update displayed contributions }); }; From 15f7ad9f8483937f3a881760b8dcc335d010bf95 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Sun, 7 Apr 2024 14:08:11 +0200 Subject: [PATCH 13/26] feat #2 : "CSS" fixes --- .../submit/submitted/submitted-panel.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/components/submit/submitted/submitted-panel.tsx b/src/components/submit/submitted/submitted-panel.tsx index cf49d74..f324e13 100644 --- a/src/components/submit/submitted/submitted-panel.tsx +++ b/src/components/submit/submitted/submitted-panel.tsx @@ -84,17 +84,16 @@ export function ContributionSubmittedPanel({ contributions }: ContributionDraftP 1 ? 'contributions' : 'contribution')} > - - - Are you sure you want to delete {deletionList.length > 1 ? 'those contributions' : 'this contribution'}? - {deletionList.length > 1 ? 'Those' : 'It'} will be permanently removed from the database. - + + Are you sure you want to delete {deletionList.length > 1 ? 'those contributions' : 'this contribution'}? + {deletionList.length > 1 ? 'Those' : 'It'} will be permanently removed from the database. + To confirm, type "DELETE" in the box below } @@ -145,10 +144,10 @@ export function ContributionSubmittedPanel({ contributions }: ContributionDraftP className="contribution-item" style={{ '--contribution-item-count': windowWidth <= BREAKPOINT_MOBILE_LARGE - ? .9 + ? .89 : windowWidth <= BREAKPOINT_DESKTOP_MEDIUM - ? 1.9 - : 2.9 + ? 1.89 + : 2.89 }} /> Date: Sun, 7 Apr 2024 14:25:14 +0200 Subject: [PATCH 14/26] packages : update prisma --- package-lock.json | 72 +++++++++++++++++++++++------------------------ package.json | 4 +-- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1624048..f67d1b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "auth-tutorial", - "version": "0.1.0", + "name": "faithful-mods", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "auth-tutorial", - "version": "0.1.0", + "name": "faithful-mods", + "version": "1.0.0", "hasInstallScript": true, "dependencies": { "@auth/prisma-adapter": "^1.0.12", @@ -21,7 +21,6 @@ "@mantine/hooks": "^7.5.3", "@mantine/modals": "^7.5.3", "@mantine/notifications": "^7.5.3", - "@prisma/client": "^5.10.2", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", @@ -55,6 +54,7 @@ "zod": "^3.22.4" }, "devDependencies": { + "@prisma/client": "^5.12.1", "@types/bcrypt": "^5.0.2", "@types/bcryptjs": "^2.4.6", "@types/multer": "^1.4.11", @@ -69,7 +69,7 @@ "postcss": "^8.4.35", "postcss-preset-mantine": "^1.13.0", "postcss-simple-vars": "^7.0.1", - "prisma": "^5.10.2", + "prisma": "^5.12.1", "stylelint": "^15.11.0", "stylelint-config-standard-scss": "^8.0.0", "stylelint-scss": "^4.7.0", @@ -1216,9 +1216,9 @@ } }, "node_modules/@prisma/client": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.11.0.tgz", - "integrity": "sha512-SWshvS5FDXvgJKM/a0y9nDC1rqd7KG0Q6ZVzd+U7ZXK5soe73DJxJJgbNBt2GNXOa+ysWB4suTpdK5zfFPhwiw==", + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.12.1.tgz", + "integrity": "sha512-6/JnizEdlSBxDIdiLbrBdMW5NqDxOmhXAJaNXiPpgzAPr/nLZResT6MMpbOHLo5yAbQ1Vv5UU8PTPRzb0WIxdA==", "hasInstallScript": true, "engines": { "node": ">=16.13" @@ -1233,48 +1233,48 @@ } }, "node_modules/@prisma/debug": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.11.0.tgz", - "integrity": "sha512-N6yYr3AbQqaiUg+OgjkdPp3KPW1vMTAgtKX6+BiB/qB2i1TjLYCrweKcUjzOoRM5BriA4idrkTej9A9QqTfl3A==", + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.12.1.tgz", + "integrity": "sha512-kd/wNsR0klrv79o1ITsbWxYyh4QWuBidvxsXSParPsYSu0ircUmNk3q4ojsgNc3/81b0ozg76iastOG43tbf8A==", "devOptional": true }, "node_modules/@prisma/engines": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.11.0.tgz", - "integrity": "sha512-gbrpQoBTYWXDRqD+iTYMirDlF9MMlQdxskQXbhARhG6A/uFQjB7DZMYocMQLoiZXO/IskfDOZpPoZE8TBQKtEw==", + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.12.1.tgz", + "integrity": "sha512-HQDdglLw2bZR/TXD2Y+YfDMvi5Q8H+acbswqOsWyq9pPjBLYJ6gzM+ptlTU/AV6tl0XSZLU1/7F4qaWa8bqpJA==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/debug": "5.11.0", - "@prisma/engines-version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102", - "@prisma/fetch-engine": "5.11.0", - "@prisma/get-platform": "5.11.0" + "@prisma/debug": "5.12.1", + "@prisma/engines-version": "5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab", + "@prisma/fetch-engine": "5.12.1", + "@prisma/get-platform": "5.12.1" } }, "node_modules/@prisma/engines-version": { - "version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102.tgz", - "integrity": "sha512-WXCuyoymvrS4zLz4wQagSsc3/nE6CHy8znyiMv8RKazKymOMd5o9FP5RGwGHAtgoxd+aB/BWqxuP/Ckfu7/3MA==", + "version": "5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab.tgz", + "integrity": "sha512-6yvO8s80Tym61aB4QNtYZfWVmE3pwqe807jEtzm8C5VDe7nw8O1FGX3TXUaXmWV0fQTIAfRbeL2Gwrndabp/0g==", "devOptional": true }, "node_modules/@prisma/fetch-engine": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.11.0.tgz", - "integrity": "sha512-994viazmHTJ1ymzvWugXod7dZ42T2ROeFuH6zHPcUfp/69+6cl5r9u3NFb6bW8lLdNjwLYEVPeu3hWzxpZeC0w==", + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.12.1.tgz", + "integrity": "sha512-qSs3KcX1HKcea1A+hlJVK/ljj0PNIUHDxAayGMvgJBqmaN32P9tCidlKz1EGv6WoRFICYnk3Dd/YFLBwnFIozA==", "devOptional": true, "dependencies": { - "@prisma/debug": "5.11.0", - "@prisma/engines-version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102", - "@prisma/get-platform": "5.11.0" + "@prisma/debug": "5.12.1", + "@prisma/engines-version": "5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab", + "@prisma/get-platform": "5.12.1" } }, "node_modules/@prisma/get-platform": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.11.0.tgz", - "integrity": "sha512-rxtHpMLxNTHxqWuGOLzR2QOyQi79rK1u1XYAVLZxDGTLz/A+uoDnjz9veBFlicrpWjwuieM4N6jcnjj/DDoidw==", + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.12.1.tgz", + "integrity": "sha512-pgIR+pSvhYHiUcqXVEZS31NrFOTENC9yFUdEAcx7cdQBoZPmHVjtjN4Ss6NzVDMYPrKJJ51U14EhEoeuBlMioQ==", "devOptional": true, "dependencies": { - "@prisma/debug": "5.11.0" + "@prisma/debug": "5.12.1" } }, "node_modules/@radix-ui/number": { @@ -7391,13 +7391,13 @@ "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" }, "node_modules/prisma": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.11.0.tgz", - "integrity": "sha512-KCLiug2cs0Je7kGkQBN9jDWoZ90ogE/kvZTUTgz2h94FEo8pczCkPH7fPNXkD1sGU7Yh65risGGD1HQ5DF3r3g==", + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.12.1.tgz", + "integrity": "sha512-SkMnb6wyIxTv9ACqiHBI2u9gD6y98qXRoCoLEnZsF6yee5Qg828G+ARrESN+lQHdw4maSZFFSBPPDpvSiVTo0Q==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "5.11.0" + "@prisma/engines": "5.12.1" }, "bin": { "prisma": "build/index.js" diff --git a/package.json b/package.json index dc576b8..8567a6c 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "@mantine/hooks": "^7.5.3", "@mantine/modals": "^7.5.3", "@mantine/notifications": "^7.5.3", - "@prisma/client": "^5.10.2", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", @@ -56,6 +55,7 @@ "zod": "^3.22.4" }, "devDependencies": { + "@prisma/client": "^5.12.1", "@types/bcrypt": "^5.0.2", "@types/bcryptjs": "^2.4.6", "@types/multer": "^1.4.11", @@ -70,7 +70,7 @@ "postcss": "^8.4.35", "postcss-preset-mantine": "^1.13.0", "postcss-simple-vars": "^7.0.1", - "prisma": "^5.10.2", + "prisma": "^5.12.1", "stylelint": "^15.11.0", "stylelint-config-standard-scss": "^8.0.0", "stylelint-scss": "^4.7.0", From 78a6a86af4cb1cfc4f0bec65850e74400824a096 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Sun, 7 Apr 2024 14:24:33 +0200 Subject: [PATCH 15/26] feat #2 : add COUNCIL role --- prisma/schema.prisma | 1 + .../(pages)/(protected)/dashboard/page.tsx | 2 +- src/app/(pages)/(protected)/layout.tsx | 2 +- src/app/(pages)/[...rest]/page.tsx | 4 +-- src/app/api/admin/route.ts | 14 ---------- src/components/auth/role-gate.tsx | 26 +++++++++++++------ 6 files changed, 23 insertions(+), 26 deletions(-) delete mode 100644 src/app/api/admin/route.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a312733..5326324 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,6 +10,7 @@ generator client { enum UserRole { ADMIN + COUNCIL USER BANNED } diff --git a/src/app/(pages)/(protected)/dashboard/page.tsx b/src/app/(pages)/(protected)/dashboard/page.tsx index 9529188..d18e902 100644 --- a/src/app/(pages)/(protected)/dashboard/page.tsx +++ b/src/app/(pages)/(protected)/dashboard/page.tsx @@ -12,7 +12,7 @@ import { gradient } from '~/lib/utils'; const DashboardPage = () => { return ( - + { return ( - + {children} ); diff --git a/src/app/(pages)/[...rest]/page.tsx b/src/app/(pages)/[...rest]/page.tsx index 4be873e..124c818 100644 --- a/src/app/(pages)/[...rest]/page.tsx +++ b/src/app/(pages)/[...rest]/page.tsx @@ -8,8 +8,8 @@ export default async function Home() { gap="md" style={{ height: 'calc(81% - (2 * var(--mantine-spacing-sm) - 62px))' }} > - 404 - Page Not Found + 404 <Text component="span" fw={300} inherit>Page Not Found</Text> + The page you are looking for does not exist ) } diff --git a/src/app/api/admin/route.ts b/src/app/api/admin/route.ts deleted file mode 100644 index a1dcf95..0000000 --- a/src/app/api/admin/route.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { UserRole } from '@prisma/client'; -import { NextResponse } from 'next/server'; - -import { currentRole } from '~/lib/auth'; - -export async function GET() { - const role = await currentRole(); - - if (role === UserRole.ADMIN) { - return new NextResponse(null, { status: 200 }); - } - - return new NextResponse(null, { status: 403 }); -} diff --git a/src/components/auth/role-gate.tsx b/src/components/auth/role-gate.tsx index 3e20a10..5b355bd 100644 --- a/src/components/auth/role-gate.tsx +++ b/src/components/auth/role-gate.tsx @@ -4,20 +4,27 @@ import { Stack, Title, Text } from '@mantine/core'; import { UserRole } from '@prisma/client'; import { useCurrentRole } from '~/hooks/use-current-role'; -import { notify } from '~/lib/utils'; interface RoleGateProps { children: React.ReactNode; - allowedRole: UserRole; + allowedRoles: UserRole[]; }; export const RoleGate = ({ children, - allowedRole, + allowedRoles, }: RoleGateProps) => { const role = useCurrentRole(); - if (role !== allowedRole && role !== UserRole.ADMIN) { + if (!allowedRoles.length) return children; + + if (allowedRoles.includes(UserRole.USER) && allowedRoles.length === 1) { + allowedRoles = Object.values(UserRole).filter(role => role !== UserRole.BANNED); + } + + if (!process.env.PRODUCTION && !allowedRoles.includes(UserRole.ADMIN)) allowedRoles.push(UserRole.ADMIN); + + if ((!role || !allowedRoles.includes(role))) { return ( - {allowedRole === UserRole.USER && role !== UserRole.BANNED ? '401' : '403'} + {!role + ? 401 <Text component="span" fw={300} inherit>Unauthorized</Text> + : 403 <Text component="span" fw={300} inherit>Forbidden</Text> + } - {allowedRole === UserRole.USER && role !== UserRole.BANNED - ? 'Unauthorized, please log in' - : 'Forbidden, you are not allowed to access this page' + {!role + ? 'Please log in to access this page' + : 'You are not allowed to access this page' } From f5a5c1caf657956e1e40e083f19c928c398da63c Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Sun, 7 Apr 2024 16:30:07 +0200 Subject: [PATCH 16/26] feat #2 : better use for the accordion --- .../(pages)/(protected)/contribute/page.tsx | 58 ++++--- src/app/globals.scss | 13 ++ src/components/submit/drafts/drafts-panel.tsx | 61 +++----- .../submit/submitted/submitted-panel.tsx | 142 ++++++++---------- 4 files changed, 141 insertions(+), 133 deletions(-) diff --git a/src/app/(pages)/(protected)/contribute/page.tsx b/src/app/(pages)/(protected)/contribute/page.tsx index c556d1f..dd7a1da 100644 --- a/src/app/(pages)/(protected)/contribute/page.tsx +++ b/src/app/(pages)/(protected)/contribute/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Card, Code, Group, Select, Stack, Text } from '@mantine/core'; +import { Accordion, Badge, Card, Code, Group, Select, Stack, Text } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { Resolution } from '@prisma/client'; import { useState, useTransition } from 'react'; @@ -47,8 +47,8 @@ const ContributePage = () => { console.error(err); notify('Error', 'Failed to fetch contributions', 'red'); }); - }) - } + }); + }; const filesDrop = (files: File[]) => { startTransition(async () => { @@ -111,21 +111,43 @@ const ContributePage = () => { - {draftContributions && - c.id).join('')} - onUpdate={reloadSubmitted} - /> - } - {contributions && - c.id).join('')} - /> - } + + + {draftContributions && + + {draftContributions.length} + }> + Drafts + + + c.id).join('')} + onUpdate={reloadSubmitted} + /> + + + } + + {contributions && + + {contributions.length} + }> + Submitted + + + c.id).join('')} + /> + + + } + - ) -} + ); +}; export default ContributePage; \ No newline at end of file diff --git a/src/app/globals.scss b/src/app/globals.scss index 13a55e8..c39ac36 100644 --- a/src/app/globals.scss +++ b/src/app/globals.scss @@ -78,4 +78,17 @@ main { .mantine-Modal-inner { left: 0; +} + +:where([data-mantine-color-scheme='dark']) .mantine-Accordion-item, +.mantine-Accordion-item { + background-color: var(--item-filled-color) !important; + box-shadow: var(--mantine-shadow-sm); + border: calc(0.0625rem * var(--mantine-scale)) solid; + border-radius: var(--accordion-radius); + border-color: var(--item-border-color); +} + +:where([data-mantine-color-scheme='light']) .mantine-Accordion-item { + background-color: var(--mantine-color-white) !important; } \ No newline at end of file diff --git a/src/components/submit/drafts/drafts-panel.tsx b/src/components/submit/drafts/drafts-panel.tsx index a12b8ef..86f84af 100644 --- a/src/components/submit/drafts/drafts-panel.tsx +++ b/src/components/submit/drafts/drafts-panel.tsx @@ -1,6 +1,6 @@ import type { Texture } from '@prisma/client'; -import { Card, Stack, Group, Badge, Text, Modal, Title, Accordion } from '@mantine/core'; +import { Stack, Group, Text, Modal, Title } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { useState } from 'react'; @@ -33,15 +33,15 @@ export function ContributionDraftPanel({ draftContributions, onUpdate }: Contrib console.error(err); notify('Error', 'Failed to fetch textures', 'red'); }); - }) + }); const openModalWithContribution = (contribution: ContributionWithCoAuthors) => { setModalContribution(contribution); openModal(); - } + }; return ( - + <> { setContributions([ ...contributions.filter((c) => c.id !== editedDraft.id), - editedDraft - ].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())) + editedDraft, + ].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())); closeModal(); }} /> } - - - {contributions.length} - }> - Drafts - - - - {contributions.length === 0 && No drafts!} - {contributions.length > 0 && These contributions are not yet submitted and only visible by you.} - - {contributions.map((contribution, index) => - { - setContributions(contributions.filter((c) => c.id !== contribution.id)); - onUpdate(); - }} - openModal={openModalWithContribution} - /> - )} - - - - - - - ) + + {contributions.length === 0 && No drafts!} + {contributions.length > 0 && These contributions are not yet submitted and only visible by you.} + + {contributions.map((contribution, index) => + { + setContributions(contributions.filter((c) => c.id !== contribution.id)); + onUpdate(); + }} + openModal={openModalWithContribution} + /> + )} + + + + ); } \ No newline at end of file diff --git a/src/components/submit/submitted/submitted-panel.tsx b/src/components/submit/submitted/submitted-panel.tsx index f324e13..4824635 100644 --- a/src/components/submit/submitted/submitted-panel.tsx +++ b/src/components/submit/submitted/submitted-panel.tsx @@ -1,4 +1,4 @@ -import { Card, Stack, Group, Badge, Text, Accordion, Checkbox, CheckboxProps, TextInput, Button, Modal } from '@mantine/core' +import { Stack, Group, Text, Checkbox, CheckboxProps, TextInput, Button, Modal } from '@mantine/core'; import { useForm } from '@mantine/form'; import { useDisclosure } from '@mantine/hooks'; import { useState, useTransition } from 'react'; @@ -75,12 +75,7 @@ export function ContributionSubmittedPanel({ contributions }: ContributionDraftP }; return ( - + <> - - - {displayedContributions.length} - }> - Submitted - - - - - - searchContribution(form.values.search)} - {...form.getInputProps('search')} - className="contribution-item" - style={{ - '--contribution-item-count': windowWidth <= BREAKPOINT_MOBILE_LARGE - ? .89 - : windowWidth <= BREAKPOINT_DESKTOP_MEDIUM - ? 1.89 - : 2.89 - }} - /> - { - setDeletionMode(event.currentTarget.checked); - if (!event.currentTarget.checked) setDeletionList([]); - }} + + + + searchContribution(form.values.search)} + {...form.getInputProps('search')} + className="contribution-item" + style={{ + '--contribution-item-count': windowWidth <= BREAKPOINT_MOBILE_LARGE + ? .89 + : windowWidth <= BREAKPOINT_DESKTOP_MEDIUM + ? 1.89 + : 2.89, + }} + /> + { + setDeletionMode(event.currentTarget.checked); + if (!event.currentTarget.checked) setDeletionList([]); + }} + /> + + + + + {displayedContributions && displayedContributions.length === 0 && Nothing yet} + {displayedContributions && displayedContributions.length > 0 && + <> + + {searchedContributions.length > 0 && searchedContributions.map((contribution, index) => + checkDeletionListFor(contribution.id)} /> - - - - - {displayedContributions && displayedContributions.length === 0 && Nothing yet} - {displayedContributions && displayedContributions.length > 0 && - <> - - {searchedContributions.length > 0 && searchedContributions.map((contribution, index) => - checkDeletionListFor(contribution.id)} - /> - )} - {searchedContributions.length === 0 && - No results found - } - - + )} + {searchedContributions.length === 0 && + No results found } - - - - - - - ) + + + } + + + + ); } \ No newline at end of file From d43f26c36737ec68a5c98fa43fee4f345c6a538d Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Sun, 7 Apr 2024 21:18:30 +0200 Subject: [PATCH 17/26] feat #2 : add council page & contribution counselor vote --- .eslintrc.json | 2 + .vscode/settings.json | 6 + .../(pages)/(protected)/contribute/page.tsx | 26 +++- src/app/(pages)/(protected)/council/page.tsx | 32 +++++ .../contributions/contribution-item.tsx | 99 +++++++++++++ .../contributions/contributions-panel.tsx | 135 ++++++++++++++++++ src/components/navbar.tsx | 11 ++ src/components/submit/drafts/drafts-modal.tsx | 20 +-- .../submit/submitted/submitted-item.tsx | 17 +-- .../submit/submitted/submitted-panel.tsx | 6 +- src/lib/auth.ts | 2 +- src/server/data/contributions.ts | 85 +++++++++-- src/server/data/polls.ts | 36 ++++- src/server/data/user.ts | 24 ++-- src/types/index.d.ts | 8 +- 15 files changed, 466 insertions(+), 43 deletions(-) create mode 100644 src/app/(pages)/(protected)/council/page.tsx create mode 100644 src/components/council/contributions/contribution-item.tsx create mode 100644 src/components/council/contributions/contributions-panel.tsx diff --git a/.eslintrc.json b/.eslintrc.json index e7e1882..7a2f9b6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,6 +3,8 @@ "rules": { "indent": ["error", "tab"], "quotes": ["error", "single"], + "semi": ["error", "always"], + "comma-dangle": ["error", "always-multiline"], "import/order": [ "error", { diff --git a/.vscode/settings.json b/.vscode/settings.json index 2fe8600..0c79879 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,8 @@ { "cSpell.words": [ "dependants", + "downvoted", + "Downvoted", "downvotes", "dropzone", "hookform", @@ -12,7 +14,11 @@ "prisma", "stylelint", "tailwindcss", + "unvoted", + "Unvoted", "unzipper", + "upvoted", + "Upvoted", "upvotes" ], "files.exclude": { diff --git a/src/app/(pages)/(protected)/contribute/page.tsx b/src/app/(pages)/(protected)/contribute/page.tsx index dd7a1da..61e67a6 100644 --- a/src/app/(pages)/(protected)/contribute/page.tsx +++ b/src/app/(pages)/(protected)/contribute/page.tsx @@ -1,8 +1,9 @@ 'use client'; -import { Accordion, Badge, Card, Code, Group, Select, Stack, Text } from '@mantine/core'; +import { Accordion, Badge, Card, Code, Group, Select, Stack, Text, Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { Resolution } from '@prisma/client'; +import Link from 'next/link'; import { useState, useTransition } from 'react'; import { CoAuthorsSelector } from '~/components/submit/co-authors-select'; @@ -12,7 +13,7 @@ import { useCurrentUser } from '~/hooks/use-current-user'; import { useDeviceSize } from '~/hooks/use-device-size'; import { useEffectOnce } from '~/hooks/use-effect-once'; import { BREAKPOINT_MOBILE_LARGE } from '~/lib/constants'; -import { notify } from '~/lib/utils'; +import { gradient, notify } from '~/lib/utils'; import { createRawContributions, getSubmittedContributions, getDraftContributions } from '~/server/data/contributions'; import type { ContributionWithCoAuthors, ContributionWithCoAuthorsAndPoll, PublicUser } from '~/types'; @@ -62,6 +63,27 @@ const ContributePage = () => { return ( + + Submission Process + + Once submitted, your submissions are subject to a voting process by the council and their decision is final.
+ When all counselors have voted, the following will happen: +
+
    + + If the contribution has more upvotes than downvotes, it will be accepted + + + If there is more downvotes or the same amount of upvotes and downvotes, it will be rejected + +
+ + When your submissions are in draft status, + you can edit them as many times as you like. But if you want to switch the texture file, please reupload it and delete your draft.
+
+ You want to join the council ? Apply here (soon). +
+ diff --git a/src/app/(pages)/(protected)/council/page.tsx b/src/app/(pages)/(protected)/council/page.tsx new file mode 100644 index 0000000..a0acf96 --- /dev/null +++ b/src/app/(pages)/(protected)/council/page.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { Card, Tabs } from '@mantine/core'; +import { UserRole } from '@prisma/client'; + +import { RoleGate } from '~/components/auth/role-gate'; +import { CouncilContributionsPanel } from '~/components/council/contributions/contributions-panel'; +import { gradient } from '~/lib/utils'; + +const CouncilPage = () => { + return ( + + + + + Contributions + + + + + + + ); +}; + +export default CouncilPage; diff --git a/src/components/council/contributions/contribution-item.tsx b/src/components/council/contributions/contribution-item.tsx new file mode 100644 index 0000000..283ba74 --- /dev/null +++ b/src/components/council/contributions/contribution-item.tsx @@ -0,0 +1,99 @@ +import { Badge, Card, Group, Image, Stack, Text } from '@mantine/core'; +import { Texture } from '@prisma/client'; +import { useTransition } from 'react'; +import { LuArrowDown, LuArrowUp, LuArrowUpDown } from 'react-icons/lu'; + +import { useCurrentUser } from '~/hooks/use-current-user'; +import { useDeviceSize } from '~/hooks/use-device-size'; +import { BREAKPOINT_MOBILE_LARGE } from '~/lib/constants'; +import { checkContributionStatus } from '~/server/data/contributions'; +import { editPollChoice } from '~/server/data/polls'; +import type { PublicUser, ContributionWithCoAuthorsAndFullPoll } from '~/types'; + + +export interface CouncilContributionItemProps { + counselors: PublicUser[]; + contribution: ContributionWithCoAuthorsAndFullPoll; + texture: Texture; + onVote: () => void; +} + +export function CouncilContributionItem({ contribution, texture, counselors, onVote }: CouncilContributionItemProps) { + const [_1, startTransition] = useTransition(); + const [windowWidth, _2] = useDeviceSize(); + const counselor = useCurrentUser()!; + + const imageStyle = { + maxWidth: 'calc(50% - var(--mantine-spacing-md))', + maxHeight: 'calc(50% - var(--mantine-spacing-md))', + minWidth: 'calc(50% - var(--mantine-spacing-md))', + minHeight: 'calc(50% - var(--mantine-spacing-md))', + }; + + const switchVote = (kind: 'up' | 'down' | 'none') => { + startTransition(() => { + editPollChoice(contribution.poll.id, counselor.id!, kind) + .then(() => { + checkContributionStatus(contribution.id); + onVote(); + }); + }); + }; + + return ( + + + + + + + + {contribution.filename} + Author: {contribution.owner.name} + Co-Authors: {contribution.coAuthors.length === 0 ? 'None' : contribution.coAuthors.map((author) => author.name).join(', ')} + + } + variant={contribution.poll.upvotes.find((v) => v.id === counselor.id) ? 'filled' : 'light'} + className="cursor-pointer" + onClick={() => switchVote('up')} + > + {contribution.poll.upvotes.length} + + } + variant={contribution.poll.downvotes.find((v) => v.id === counselor.id) ? 'filled' : 'light'} + className="cursor-pointer" + onClick={() => switchVote('down')} + > + {contribution.poll.downvotes.length} + + } + variant={contribution.poll.upvotes.find((v) => v.id === counselor.id) === undefined && contribution.poll.downvotes.find((v) => v.id === counselor.id) === undefined ? 'filled' : 'light'} + className="cursor-pointer" + onClick={() => switchVote('none')} + > + {counselors.length - (contribution.poll.upvotes.length + contribution.poll.downvotes.length)} + + + + + + ); +} \ No newline at end of file diff --git a/src/components/council/contributions/contributions-panel.tsx b/src/components/council/contributions/contributions-panel.tsx new file mode 100644 index 0000000..1eeea79 --- /dev/null +++ b/src/components/council/contributions/contributions-panel.tsx @@ -0,0 +1,135 @@ +'use client'; + +import { Accordion, Badge, Card, Group, Stack, Text, Title } from '@mantine/core'; +import { Texture } from '@prisma/client'; +import { useState } from 'react'; + +import { useCurrentUser } from '~/hooks/use-current-user'; +import { useEffectOnce } from '~/hooks/use-effect-once'; +import { getPendingContributions } from '~/server/data/contributions'; +import { getTextures } from '~/server/data/texture'; +import { getCounselors } from '~/server/data/user'; +import { ContributionWithCoAuthorsAndFullPoll, PublicUser } from '~/types'; + +import { CouncilContributionItem } from './contribution-item'; + +export function CouncilContributionsPanel() { + const [textures, setTextures] = useState([]); + const [counselors, setCounselors] = useState([]); + + const [counselorVoted, setCounselorVoted] = useState([]); + const [counselorUnvoted, setCounselorUnvoted] = useState([]); + + const counselor = useCurrentUser()!; + + useEffectOnce(() => { + getTextures() + .then(setTextures) + .catch((err) => console.error(err)); + + loadContributions(); + + getCounselors() + .then(setCounselors) + .catch((err) => console.error(err)); + }); + + const loadContributions = () => { + getPendingContributions() + .then((res) => { + const unvoted: ContributionWithCoAuthorsAndFullPoll[] = []; + const voted: ContributionWithCoAuthorsAndFullPoll[] = []; + + res.forEach((c) => { + if (c.poll.downvotes.find((dv) => dv.id === counselor.id) || c.poll.upvotes.find((uv) => uv.id === counselor.id)) + voted.push(c); + else + unvoted.push(c); + }); + + setCounselorUnvoted(unvoted); + setCounselorVoted(voted); + }) + .catch((err) => console.error(err)); + }; + + return ( + + + Submission Process + + Once all counselors have voted: + +
    + + If the contribution has more upvotes than downvotes, it will be accepted + + + If there is more downvotes or the same amount of upvotes and downvotes, it will be rejected + + + Once the voted is ended, the contribution status will be definitive. + +
+ + There is actually {counselorUnvoted.length + counselorVoted.length} contribution(s) in the voting process + and {counselors.length} counselor(s) in the council. + +
+ + + + {counselorUnvoted.length}} + >Need your vote + + + + { + counselorUnvoted.length === 0 + ? 'No more contributions waiting for you, nice!' + : 'Please vote for the following contributions:' + } + + {counselorUnvoted.map((c, i) => + t.id === c.textureId)!} + onVote={loadContributions} + /> + )} + + + + + + {counselorVoted.length}} + >Voted! + + + + { + counselorVoted.length === 0 + ? 'No more contributions waiting in the voting process, nice!' + : 'Here you can edit your votes:' + } + + {counselorVoted.map((c, i) => + t.id === c.textureId)!} + onVote={loadContributions} + /> + )} + + + + +
+ ); +} \ No newline at end of file diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index 7452d11..771a8b4 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -4,6 +4,7 @@ import { ActionIcon, Badge, Button, Card, Combobox, Group, Image, useCombobox, } import { UserRole } from '@prisma/client'; import Link from 'next/link'; import { usePathname, useRouter } from 'next/navigation'; +import { GoLaw } from 'react-icons/go'; import { HiOutlineMenu } from 'react-icons/hi'; import { IoMdSettings } from 'react-icons/io'; import { MdDashboard } from 'react-icons/md'; @@ -114,6 +115,16 @@ export const Navbar = () => { > } + + {user && user.role === UserRole.COUNCIL && + + + + } {user && (user.role !== UserRole.BANNED || windowWidth !== BREAKPOINT_TABLET) && diff --git a/src/components/submit/drafts/drafts-modal.tsx b/src/components/submit/drafts/drafts-modal.tsx index 012b088..4910ea7 100644 --- a/src/components/submit/drafts/drafts-modal.tsx +++ b/src/components/submit/drafts/drafts-modal.tsx @@ -61,8 +61,8 @@ export function ContributionDraftModal({ contribution, textures, onClose }: Cont setSelectedTextureContributionsIndex(0); setDisplayedSelectedTextureContributions(res[0]); }) - .catch(console.error) - } + .catch(console.error); + }; const renderMultiSelectOption: MultiSelectProps['renderOption'] = ({ option }) => { const texture = textures.find((u) => u.id === option.value)!; @@ -84,21 +84,21 @@ export function ContributionDraftModal({ contribution, textures, onClose }: Cont */ const sanitizeTextureName = (name: string): string => { return name.split('_')[1]?.split('.')[0]; - } + }; const previousContribution = () => { if (selectedTextureContributionsIndex === 0) return; let index = selectedTextureContributionsIndex - 1; setSelectedTextureContributionsIndex(index); setDisplayedSelectedTextureContributions(selectedTextureContributions[index]); - } + }; const nextContribution = () => { if (selectedTextureContributionsIndex === selectedTextureContributions.length - 1) return; let index = selectedTextureContributionsIndex + 1; setSelectedTextureContributionsIndex(index); setDisplayedSelectedTextureContributions(selectedTextureContributions[index]); - } + }; const updateDraft = () => { if (!selectedTexture) return; @@ -112,13 +112,13 @@ export function ContributionDraftModal({ contribution, textures, onClose }: Cont textureId: selectedTexture.id, }) .then(onClose) - .catch(console.error) - }) - } + .catch(console.error); + }); + }; const cancelAndClose = () => { onClose(contribution); - } + }; return ( @@ -292,5 +292,5 @@ export function ContributionDraftModal({ contribution, textures, onClose }: Cont
- ) + ); } \ No newline at end of file diff --git a/src/components/submit/submitted/submitted-item.tsx b/src/components/submit/submitted/submitted-item.tsx index cb3fa97..9d3a778 100644 --- a/src/components/submit/submitted/submitted-item.tsx +++ b/src/components/submit/submitted/submitted-item.tsx @@ -1,8 +1,9 @@ import { Badge, Card, Group, Image, Stack, Text } from '@mantine/core'; -import { Poll, Status } from '@prisma/client'; +import { Status } from '@prisma/client'; import { ClassValue } from 'clsx'; import { useState } from 'react'; -import { FaArrowUp, FaArrowDown, FaFileAlt } from 'react-icons/fa'; +import { FaFileAlt } from 'react-icons/fa'; +import { LuArrowUp, LuArrowDown } from 'react-icons/lu'; import { useDeviceSize } from '~/hooks/use-device-size'; import { useEffectOnce } from '~/hooks/use-effect-once'; @@ -24,7 +25,7 @@ export function ContributionSubmittedItem({ contribution, className, onClick }: useEffectOnce(() => { getPollResult(contribution.pollId).then((res) => setPoll(res)); - }) + }); return ( @@ -81,20 +82,20 @@ export function ContributionSubmittedItem({ contribution, className, onClick }: > {windowWidth <= BREAKPOINT_TABLET ? contribution.status.slice(0, 1) : contribution.status} - {poll && contribution.status === Status.ACCEPTED && + {poll && contribution.status !== Status.PENDING && {poll.upvotes} - + {poll.downvotes} - + }
- ) + ); } \ No newline at end of file diff --git a/src/components/submit/submitted/submitted-panel.tsx b/src/components/submit/submitted/submitted-panel.tsx index 4824635..b380485 100644 --- a/src/components/submit/submitted/submitted-panel.tsx +++ b/src/components/submit/submitted/submitted-panel.tsx @@ -131,10 +131,10 @@ export function ContributionSubmittedPanel({ contributions }: ContributionDraftP className="contribution-item" style={{ '--contribution-item-count': windowWidth <= BREAKPOINT_MOBILE_LARGE - ? .89 + ? 1 : windowWidth <= BREAKPOINT_DESKTOP_MEDIUM - ? 1.89 - : 2.89, + ? 1.85 + : 2.885, }} /> { @@ -13,7 +14,11 @@ export async function getSubmittedContributions(ownerId: string): Promise { return await db.contribution.findMany({ where: { textureId, status: Status.ACCEPTED }, - include: { coAuthors: { select: { id: true, name: true, image: true } }, poll: true }, + include: { + coAuthors: { select: { id: true, name: true, image: true } }, + owner: { select: { id: true, name: true, image: true } }, + poll: true, + }, }); } @@ -57,7 +66,10 @@ export async function getDraftContributions(ownerId: string): Promise remove(file as `files/${string}`))); await db.contribution.deleteMany({ where: { id: { in: ids } } }); } + +export async function getPendingContributions(): Promise { + await canAccess(UserRole.COUNCIL); + + return await db.contribution.findMany({ + where: { status: Status.PENDING }, + include: { + coAuthors: { select: { id: true, name: true, image: true } }, + owner: { select: { id: true, name: true, image: true } }, + poll: { + select: { + id: true, + downvotes: { select: { id: true, name: true, image: true } }, + upvotes: { select: { id: true, name: true, image: true } }, + createdAt: true, + updatedAt: true, + }, + }, + }, + }); +} + +export async function checkContributionStatus(contributionId: string) { + await canAccess(UserRole.COUNCIL); + + const counselors = await getCounselors(); + const contribution = await db.contribution.findFirstOrThrow({ + where: { id: contributionId }, + include: { + poll: { + select: { + upvotes: { select: { id: true } }, + downvotes: { select: { id: true } }, + }, + }, + }, + }); + + // voting period ended + if (contribution.poll.upvotes.length + contribution.poll.downvotes.length === counselors.length) { + if (contribution.poll.upvotes.length > contribution.poll.downvotes.length) { + await db.contribution.update({ + where: { id: contributionId }, + data: { status: Status.ACCEPTED }, + }); + } + else { + await db.contribution.update({ + where: { id: contributionId }, + data: { status: Status.REJECTED }, + }); + } + } +} diff --git a/src/server/data/polls.ts b/src/server/data/polls.ts index b1f7172..036c958 100644 --- a/src/server/data/polls.ts +++ b/src/server/data/polls.ts @@ -9,4 +9,38 @@ export async function getPollResult(id: string): Promise { upvotes: res.upvotes.length, downvotes: res.downvotes.length, }; -} \ No newline at end of file +} + +export async function editPollChoice(pollId: string, userId: string, kind: 'up' | 'down' | 'none') { + switch(kind) { + case 'up': + await db.poll.update({ + where: { id: pollId }, + data: { + upvotes: { connect: { id: userId } }, + downvotes: { disconnect: { id: userId } }, + }, + }); + break; + + case 'down': + await db.poll.update({ + where: { id: pollId }, + data: { + upvotes: { disconnect: { id: userId } }, + downvotes: { connect: { id: userId } }, + }, + }); + break; + + case 'none': + await db.poll.update({ + where: { id: pollId }, + data: { + upvotes: { disconnect: { id: userId } }, + downvotes: { disconnect: { id: userId } }, + }, + }); + break; + } +} diff --git a/src/server/data/user.ts b/src/server/data/user.ts index 18ac134..1441dc6 100644 --- a/src/server/data/user.ts +++ b/src/server/data/user.ts @@ -16,15 +16,11 @@ export async function getUsers(): Promise { } export async function getPublicUsers(): Promise { - const users = await db.user.findMany({ where: { role: { not: UserRole.BANNED }}, orderBy: { name: 'asc' }}); - - return users - .filter((user) => user.name !== null) - .map((user) => ({ - id: user.id, - name: user.name ?? 'Unknown', // This should never happen (filtered above) - image: user.image ?? '/icon.png', - })); + return await db.user.findMany({ + where: { role: { not: UserRole.BANNED } }, + select: { id: true, name: true, image: true }, + orderBy: { name: 'asc' }, + }); } /** @@ -75,3 +71,13 @@ export async function getUserById(id: string): Promise { return res; }; + +export async function getCounselors(): Promise { + await canAccess(UserRole.COUNCIL); + + return await db.user.findMany({ + where: { role: UserRole.COUNCIL }, + select: { id: true, name: true, image: true }, + orderBy: { name: 'asc' }, + }); +} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index edde67c..11c2234 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -24,8 +24,14 @@ export type PublicUser = { image: string | null; }; -export type ContributionWithCoAuthors = Contribution & { coAuthors: PublicUser[] }; +export type FullPoll = Poll & { + downvotes: PublicUser[]; + upvotes: PublicUser[]; +} + +export type ContributionWithCoAuthors = Contribution & { coAuthors: PublicUser[], owner: PublicUser }; export type ContributionWithCoAuthorsAndPoll = ContributionWithCoAuthors & { poll: Poll }; +export type ContributionWithCoAuthorsAndFullPoll = ContributionWithCoAuthors & { poll: FullPoll }; export type Progression = { linkedTextures: number; From b276deb58b6af4d9cd294e3b8d90cf2fc1c608a4 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Sun, 7 Apr 2024 22:07:17 +0200 Subject: [PATCH 18/26] fix: better name for many-to-many relations tables --- prisma/schema.prisma | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5326324..f3336fb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -69,7 +69,7 @@ model ModpackVersion { version String modpack Modpack @relation(fields: [modpackId], references: [id]) modpackId String @map("modpack_id") - mods ModVersion[] + mods ModVersion[] @relation("mods_versions_to_modpacks_versions") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -101,7 +101,7 @@ model ModVersion { modId String @map("mod_id") resources Resource[] - ModpackVersion ModpackVersion[] + ModpackVersion ModpackVersion[] @relation("mods_versions_to_modpacks_versions") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -192,7 +192,7 @@ model Contribution { ownerId String owner User @relation(fields: [ownerId], references: [id]) - coAuthors User[] @relation("users") + coAuthors User[] @relation("contributions_to_coauthors") resolution Resolution status Status @default(DRAFT) @@ -212,7 +212,7 @@ model Contribution { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - @@map("users_contributions") + @@map("contributions") } model User { @@ -224,26 +224,26 @@ model User { role UserRole @default(USER) accounts Account[] - ownedContributions Contribution[] - contributions Contribution[] @relation("users") + contributions Contribution[] + coContributions Contribution[] @relation("contributions_to_coauthors") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - pollsUpvoted Poll[] @relation("Upvotes") - pollsDownvoted Poll[] @relation("Downvotes") + pollsUpvoted Poll[] @relation("polls_upvotes_to_users") + pollsDownvoted Poll[] @relation("polls_downvotes_to_users") @@map("users") } model Poll { id String @id @default(cuid()) - upvotes User[] @relation("Upvotes") - downvotes User[] @relation("Downvotes") + upvotes User[] @relation("polls_upvotes_to_users") + downvotes User[] @relation("polls_downvotes_to_users") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") Contribution Contribution? - @@map("users_polls") + @@map("polls") } From 8e49d7a439cbde34e6556e452a73a6c9f7d7a829 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Sun, 7 Apr 2024 22:07:36 +0200 Subject: [PATCH 19/26] feat #2: add co-submitted accordion to submitted page --- .../(pages)/(protected)/contribute/page.tsx | 29 ++++++++++++++- .../submit/submitted/submitted-panel.tsx | 13 +++++-- src/server/data/contributions.ts | 36 ++++++++++++++++--- 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/src/app/(pages)/(protected)/contribute/page.tsx b/src/app/(pages)/(protected)/contribute/page.tsx index 61e67a6..1f6532d 100644 --- a/src/app/(pages)/(protected)/contribute/page.tsx +++ b/src/app/(pages)/(protected)/contribute/page.tsx @@ -14,7 +14,7 @@ import { useDeviceSize } from '~/hooks/use-device-size'; import { useEffectOnce } from '~/hooks/use-effect-once'; import { BREAKPOINT_MOBILE_LARGE } from '~/lib/constants'; import { gradient, notify } from '~/lib/utils'; -import { createRawContributions, getSubmittedContributions, getDraftContributions } from '~/server/data/contributions'; +import { createRawContributions, getSubmittedContributions, getDraftContributions, getCoSubmittedContributions } from '~/server/data/contributions'; import type { ContributionWithCoAuthors, ContributionWithCoAuthorsAndPoll, PublicUser } from '~/types'; const ContributePage = () => { @@ -26,6 +26,7 @@ const ContributePage = () => { const [contributions, setContributions] = useState(); const [draftContributions, setDraftContributions] = useState(); + const [coContributions, setCoContributions] = useState(); const user = useCurrentUser()!; // the user is guaranteed to be logged in (per the layout) @@ -37,6 +38,13 @@ const ContributePage = () => { notify('Error', 'Failed to fetch draft contributions', 'red'); }); + getCoSubmittedContributions(user.id!) + .then(setCoContributions) + .catch((err) => { + console.error(err); + notify('Error', 'Failed to fetch submitted contributions', 'red'); + }); + reloadSubmitted(); }); @@ -160,6 +168,7 @@ const ContributePage = () => { Submitted + Contributions you own and have submitted. c.id).join('')} @@ -167,6 +176,24 @@ const ContributePage = () => { } + + {coContributions && + + {coContributions.length} + }> + Co-Submitted + + + Contributions where you appear as a co-author. + c.id).join('')} + /> + + + } ); diff --git a/src/components/submit/submitted/submitted-panel.tsx b/src/components/submit/submitted/submitted-panel.tsx index b380485..a698394 100644 --- a/src/components/submit/submitted/submitted-panel.tsx +++ b/src/components/submit/submitted/submitted-panel.tsx @@ -18,9 +18,10 @@ import '../submit.scss'; export interface ContributionDraftPanelProps { contributions: ContributionWithCoAuthorsAndPoll[]; + coSubmitted?: boolean; } -export function ContributionSubmittedPanel({ contributions }: ContributionDraftPanelProps) { +export function ContributionSubmittedPanel({ contributions, coSubmitted }: ContributionDraftPanelProps) { const [windowWidth, _] = useDeviceSize(); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [isPending, startTransition] = useTransition(); @@ -83,8 +84,14 @@ export function ContributionSubmittedPanel({ contributions }: ContributionDraftP title={'Delete ' + (deletionList.length > 1 ? 'contributions' : 'contribution')} > - Are you sure you want to delete {deletionList.length > 1 ? 'those contributions' : 'this contribution'}? - {deletionList.length > 1 ? 'Those' : 'It'} will be permanently removed from the database. + Are you sure you want {coSubmitted ? 'to be removed from' : 'to delete'} {deletionList.length > 1 ? 'those contributions' : 'this contribution'}? + + { + coSubmitted + ? 'You will be removed from the list of co-authors.' + : (deletionList.length > 1 ? 'Those' : 'It') + ' will be permanently removed from the database.' + } + { + return await db.contribution.findMany({ + where: { coAuthors: { some: { id: coAuthorId } }, status: { not: Status.DRAFT } }, + include: { + coAuthors: { select: { id: true, name: true, image: true } }, + owner: { select: { id: true, name: true, image: true } }, + poll: true, + }, + }); +} + export async function getDraftContributions(ownerId: string): Promise { await canAccess(UserRole.ADMIN, ownerId); @@ -113,12 +124,27 @@ export async function submitContribution(ownerId: string, id: string) { export async function deleteContributions(ownerId: string, ...ids: string[]): Promise { await canAccess(UserRole.ADMIN, ownerId); - - const contributions = await db.contribution.findMany({ where: { id: { in: ids } } }); - const contributionFiles = contributions.map((c) => c.file); - await Promise.all(contributionFiles.map((file) => remove(file as `files/${string}`))); - await db.contribution.deleteMany({ where: { id: { in: ids } } }); + const contributions = await db.contribution.findMany({ + where: { id: { in: ids } }, + include: { coAuthors: { select: { id: true } } }, + }); + + for (const contribution of contributions) { + // Case co-author wants to be removed from the contribution + if (contribution.ownerId !== ownerId && contribution.coAuthors.map((c) => c.id).includes(ownerId)) { + await db.contribution.update({ + where: { id: contribution.id }, + data: { coAuthors: { disconnect: { id: ownerId } } }, + }); + } + + // Base case: owner wants to delete the contribution + if (contribution.ownerId === ownerId) { + await remove(contribution.file as `files/${string}`); + await db.contribution.delete({ where: { id: contribution.id } }); + } + } } export async function getPendingContributions(): Promise { From 21a71518d05e00359c59b20d61e68dc4e182b90f Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Sun, 7 Apr 2024 22:12:06 +0200 Subject: [PATCH 20/26] fix: add new ESLint rules --- src/app/(pages)/(protected)/layout.tsx | 2 +- .../(protected)/settings/[userId]/page.tsx | 2 +- src/app/(pages)/[...rest]/page.tsx | 2 +- src/app/(pages)/layout.tsx | 10 ++++----- src/app/(pages)/modpacks/page.tsx | 6 ++--- src/app/(pages)/mods/page.tsx | 6 ++--- src/app/(pages)/page.tsx | 6 ++--- src/app/api/auth/[...nextauth]/route.ts | 2 +- src/auth.config.ts | 4 ++-- src/components/auth/logged-user.tsx | 2 +- src/components/auth/role-gate.tsx | 2 +- src/components/dashboard/dashboard-item.tsx | 8 +++---- .../modpacks/modal/modpack-general.tsx | 2 +- .../modpacks/modal/modpack-modal.tsx | 12 +++++----- .../modpack-version-modal.tsx | 22 +++++++++---------- .../modpack-versions/modpack-version.tsx | 8 +++---- .../dashboard/modpacks/modpacks-panel.tsx | 2 +- .../modal/mod-versions/mod-version-modal.tsx | 10 ++++----- .../mods/modal/mod-versions/mod-version.tsx | 12 +++++----- .../dashboard/mods/modal/mods-general.tsx | 2 +- .../dashboard/mods/modal/mods-modal.tsx | 16 +++++++------- src/components/dashboard/mods/mods-panel.tsx | 4 ++-- .../progression/progression-item.tsx | 10 ++++----- .../progression/progression-panel.tsx | 6 ++--- .../dashboard/users/users-panel.tsx | 2 +- src/components/navbar.tsx | 4 ++-- src/components/settings/user-panel.tsx | 12 +++++----- src/components/submit/drafts/drafts-item.tsx | 10 ++++----- src/components/theme-switch.tsx | 8 +++---- src/lib/utils.ts | 22 +++++++++---------- src/middleware.ts | 4 ++-- src/server/actions/files.ts | 4 ++-- src/server/data/mods-version.ts | 12 +++++----- src/server/data/texture.ts | 8 +++---- 34 files changed, 122 insertions(+), 122 deletions(-) diff --git a/src/app/(pages)/(protected)/layout.tsx b/src/app/(pages)/(protected)/layout.tsx index 6b4d348..9955749 100644 --- a/src/app/(pages)/(protected)/layout.tsx +++ b/src/app/(pages)/(protected)/layout.tsx @@ -12,6 +12,6 @@ const ProtectedLayout = ({ children }: ProtectedLayoutProps) => { {children} ); -} +}; export default ProtectedLayout; \ No newline at end of file diff --git a/src/app/(pages)/(protected)/settings/[userId]/page.tsx b/src/app/(pages)/(protected)/settings/[userId]/page.tsx index 9f998b8..7634eab 100644 --- a/src/app/(pages)/(protected)/settings/[userId]/page.tsx +++ b/src/app/(pages)/(protected)/settings/[userId]/page.tsx @@ -18,6 +18,6 @@ const SettingsPage = () => { ); -} +}; export default SettingsPage; \ No newline at end of file diff --git a/src/app/(pages)/[...rest]/page.tsx b/src/app/(pages)/[...rest]/page.tsx index 124c818..6239bfe 100644 --- a/src/app/(pages)/[...rest]/page.tsx +++ b/src/app/(pages)/[...rest]/page.tsx @@ -11,5 +11,5 @@ export default async function Home() { 404 <Text component="span" fw={300} inherit>Page Not Found</Text> The page you are looking for does not exist - ) + ); } diff --git a/src/app/(pages)/layout.tsx b/src/app/(pages)/layout.tsx index 9375cb0..e76151a 100644 --- a/src/app/(pages)/layout.tsx +++ b/src/app/(pages)/layout.tsx @@ -1,10 +1,10 @@ -import type { Metadata } from 'next' +import type { Metadata } from 'next'; import { ColorSchemeScript, createTheme, MantineProvider } from '@mantine/core'; import { Notifications } from '@mantine/notifications'; -import { SessionProvider } from 'next-auth/react' +import { SessionProvider } from 'next-auth/react'; -import { auth } from '~/auth' +import { auth } from '~/auth'; import { Navbar } from '~/components/navbar'; import { BREAKPOINT_DESKTOP_LARGE } from '~/lib/constants'; @@ -12,11 +12,11 @@ import { BREAKPOINT_DESKTOP_LARGE } from '~/lib/constants'; // All packages except `@mantine/hooks` require styles imports import '@mantine/core/styles.css'; import '@mantine/dropzone/styles.css'; -import '../globals.scss' +import '../globals.scss'; export const metadata: Metadata = { title: 'Faithful Mods', -} +}; const theme = createTheme({ cursorType: 'pointer', diff --git a/src/app/(pages)/modpacks/page.tsx b/src/app/(pages)/modpacks/page.tsx index 0fecd38..4638c6a 100644 --- a/src/app/(pages)/modpacks/page.tsx +++ b/src/app/(pages)/modpacks/page.tsx @@ -4,13 +4,13 @@ import { cn } from '~/lib/utils'; const font = Poppins({ subsets: ['latin'], - weight: ['600'] -}) + weight: ['600'], +}); export default async function Modpacks() { return (

Modpacks page

- ) + ); } diff --git a/src/app/(pages)/mods/page.tsx b/src/app/(pages)/mods/page.tsx index 91973d2..17fbd2b 100644 --- a/src/app/(pages)/mods/page.tsx +++ b/src/app/(pages)/mods/page.tsx @@ -4,13 +4,13 @@ import { cn } from '~/lib/utils'; const font = Poppins({ subsets: ['latin'], - weight: ['600'] -}) + weight: ['600'], +}); export default async function Mods() { return (

Mods page

- ) + ); } diff --git a/src/app/(pages)/page.tsx b/src/app/(pages)/page.tsx index def5119..d598706 100644 --- a/src/app/(pages)/page.tsx +++ b/src/app/(pages)/page.tsx @@ -4,8 +4,8 @@ import { cn } from '~/lib/utils'; const font = Poppins({ subsets: ['latin'], - weight: ['600'] -}) + weight: ['600'], +}); export default async function Home() { return ( @@ -13,5 +13,5 @@ export default async function Home() {

Faithful Mods homepage

{ process.env.PRODUCTION ?

Production mode

:

Development mode

} - ) + ); } diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 2110e1b..e556635 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1 +1 @@ -export { GET, POST } from '~/auth' +export { GET, POST } from '~/auth'; diff --git a/src/auth.config.ts b/src/auth.config.ts index 475820d..d2a6c18 100644 --- a/src/auth.config.ts +++ b/src/auth.config.ts @@ -10,6 +10,6 @@ export default { Github({ clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, - }) + }), ], -} satisfies NextAuthConfig \ No newline at end of file +} satisfies NextAuthConfig; \ No newline at end of file diff --git a/src/components/auth/logged-user.tsx b/src/components/auth/logged-user.tsx index 52ec86a..a36bd02 100644 --- a/src/components/auth/logged-user.tsx +++ b/src/components/auth/logged-user.tsx @@ -18,7 +18,7 @@ export const LoggedUser = () => { useEffectOnce(() => { if (user) setUserPicture(user.image ?? undefined); - }) + }); return ( - ) + ); } return children; diff --git a/src/components/dashboard/dashboard-item.tsx b/src/components/dashboard/dashboard-item.tsx index 768efeb..a53bb91 100644 --- a/src/components/dashboard/dashboard-item.tsx +++ b/src/components/dashboard/dashboard-item.tsx @@ -1,9 +1,9 @@ -import { Group, Image, Stack, Text } from '@mantine/core' +import { Group, Image, Stack, Text } from '@mantine/core'; import { useDeviceSize } from '~/hooks/use-device-size'; import { BREAKPOINT_DESKTOP_LARGE, BREAKPOINT_DESKTOP_MEDIUM, BREAKPOINT_MOBILE_LARGE } from '~/lib/constants'; -import './dashboard.scss' +import './dashboard.scss'; export interface ItemDisplayProps { image?: string | null, @@ -28,7 +28,7 @@ export function DashboardItem({ image, title, description, onClick }: ItemDispla ? 2 : windowWidth <= BREAKPOINT_DESKTOP_LARGE ? 3 - : 4 + : 4, }} > {description?.trim() ?? 'No description'}
- ) + ); } diff --git a/src/components/dashboard/modpacks/modal/modpack-general.tsx b/src/components/dashboard/modpacks/modal/modpack-general.tsx index aa202d0..8c00f77 100644 --- a/src/components/dashboard/modpacks/modal/modpack-general.tsx +++ b/src/components/dashboard/modpacks/modal/modpack-general.tsx @@ -28,5 +28,5 @@ export function ModpackModalGeneral({ previewImg, modpack, form }: { form: UseFo
- ) + ); } \ No newline at end of file diff --git a/src/components/dashboard/modpacks/modal/modpack-modal.tsx b/src/components/dashboard/modpacks/modal/modpack-modal.tsx index e1578b1..557bde9 100644 --- a/src/components/dashboard/modpacks/modal/modpack-modal.tsx +++ b/src/components/dashboard/modpacks/modal/modpack-modal.tsx @@ -44,7 +44,7 @@ export function ModpackModal({ modpack, onClose }: { modpack?: Modpack | undefin }, }, onValuesChange: (value) => { - if (value.image && value.image instanceof File) setPreviewImg(value.image ? URL.createObjectURL(value.image) : modpack?.image || '') + if (value.image && value.image instanceof File) setPreviewImg(value.image ? URL.createObjectURL(value.image) : modpack?.image || ''); }, }); @@ -72,15 +72,15 @@ export function ModpackModal({ modpack, onClose }: { modpack?: Modpack | undefin } catch (e) { notify('Error', (e as Error).message, 'red'); } - }) - } + }); + }; const onDelete = (id: string) => { startTransition(async () => { deleteModpack(id); onClose(id); - }) - } + }); + }; return ( <> @@ -122,5 +122,5 @@ export function ModpackModal({ modpack, onClose }: { modpack?: Modpack | undefin - ) + ); } \ No newline at end of file diff --git a/src/components/dashboard/modpacks/modal/modpack-versions/modpack-version-modal.tsx b/src/components/dashboard/modpacks/modal/modpack-versions/modpack-version-modal.tsx index 5d1224b..b503072 100644 --- a/src/components/dashboard/modpacks/modal/modpack-versions/modpack-version-modal.tsx +++ b/src/components/dashboard/modpacks/modal/modpack-versions/modpack-version-modal.tsx @@ -34,8 +34,8 @@ export function ModpackVersionModal({ modpack, modpackVersion, onClose }: { modp version: (value) => { if (!value) return 'Version is required'; return null; - } - } + }, + }, }); const saveMV = () => { @@ -43,13 +43,13 @@ export function ModpackVersionModal({ modpack, modpackVersion, onClose }: { modp if (!_modpackVersion) return; // New modpack version - if (!modpackVersion) await updateModpackVersion({ id: _modpackVersion.id, version: form.values.version }) + if (!modpackVersion) await updateModpackVersion({ id: _modpackVersion.id, version: form.values.version }); // Edit of an existing modpack version else await updateModpackVersion({ id: modpackVersion.id, version: form.values.version }); onClose(); - }) - } + }); + }; const deleteMV = () => { startTransition(async () => { @@ -57,7 +57,7 @@ export function ModpackVersionModal({ modpack, modpackVersion, onClose }: { modp await deleteModpackVersion(modpackVersion.id); onClose(); }); - } + }; const deleteModFromMV = (modVersionId: string) => { startTransition(async () => { @@ -67,10 +67,10 @@ export function ModpackVersionModal({ modpack, modpackVersion, onClose }: { modp setModpackVersion(updated); setModVersions(updated.mods); - const mods = await getModsFromIds(updated.mods.map((modVersion) => modVersion.modId)) + const mods = await getModsFromIds(updated.mods.map((modVersion) => modVersion.modId)); setMods(mods); }); - } + }; const filesDrop = (files: File[]) => { startTransition(async () => { @@ -82,11 +82,11 @@ export function ModpackVersionModal({ modpack, modpackVersion, onClose }: { modp const data = new FormData(); files.forEach((file) => data.append('file', file)); - const updated = await addModsToModpackVersion(editedModpackVersion.id, data) + const updated = await addModsToModpackVersion(editedModpackVersion.id, data); setModpackVersion(updated); setModVersions(updated.mods); - const mods = await getModsFromIds(updated.mods.map((modVersion) => modVersion.modId)) + const mods = await getModsFromIds(updated.mods.map((modVersion) => modVersion.modId)); setMods(mods); } catch (e) { notify('Error', (e as Error).message, 'red'); @@ -163,5 +163,5 @@ export function ModpackVersionModal({ modpack, modpackVersion, onClose }: { modp - ) + ); } \ No newline at end of file diff --git a/src/components/dashboard/modpacks/modal/modpack-versions/modpack-version.tsx b/src/components/dashboard/modpacks/modal/modpack-versions/modpack-version.tsx index ba407e1..d5e1be0 100644 --- a/src/components/dashboard/modpacks/modal/modpack-versions/modpack-version.tsx +++ b/src/components/dashboard/modpacks/modal/modpack-versions/modpack-version.tsx @@ -25,17 +25,17 @@ export function ModpackVersions({ modpack }: { modpack: Modpack }) { console.error(error); notify('Error', error.message, 'red'); }); - }) + }); const openModpackVersionModal = (modpackVersion?: ModpackVersionWithMods | undefined) => { setModalModpackVersion(modpackVersion); openModal(); - } + }; const closeModpackVersionModal = async () => { setModpackVersions(await getModpackVersions(modpack.id)); closeModal(); - } + }; return ( <> @@ -83,5 +83,5 @@ export function ModpackVersions({ modpack }: { modpack: Modpack }) { - ) + ); } \ No newline at end of file diff --git a/src/components/dashboard/modpacks/modpacks-panel.tsx b/src/components/dashboard/modpacks/modpacks-panel.tsx index 5b29db3..d98b6d6 100644 --- a/src/components/dashboard/modpacks/modpacks-panel.tsx +++ b/src/components/dashboard/modpacks/modpacks-panel.tsx @@ -38,7 +38,7 @@ export function ModpacksPanel() { console.error(err); notify('Error', err.message, 'red'); }); - } + }; const searchModpack = (search: string) => { if (!modpacks) return; diff --git a/src/components/dashboard/mods/modal/mod-versions/mod-version-modal.tsx b/src/components/dashboard/mods/modal/mod-versions/mod-version-modal.tsx index ac29b0b..65f62ad 100644 --- a/src/components/dashboard/mods/modal/mod-versions/mod-version-modal.tsx +++ b/src/components/dashboard/mods/modal/mod-versions/mod-version-modal.tsx @@ -33,8 +33,8 @@ export function ModVersionModal({ mod, modVersion, onClose }: { mod: Mod, modVer if (!value) return 'MC Version is required'; if (value === 'unknown') return 'MC Version cannot be unknown'; return null; - } - } + }, + }, }); const saveMV = () => { @@ -43,7 +43,7 @@ export function ModVersionModal({ mod, modVersion, onClose }: { mod: Mod, modVer await updateModVersion({ id: modVersion.id, version: form.values.version, mcVersion: form.values.mcVersion }); onClose(); - }) + }); }; const deleteMV = () => { @@ -52,7 +52,7 @@ export function ModVersionModal({ mod, modVersion, onClose }: { mod: Mod, modVer await deleteModVersion(modVersion.id); onClose(); - }) + }); }; const deleteModpackFromMV = (modpackVersionId: string) => { @@ -62,7 +62,7 @@ export function ModVersionModal({ mod, modVersion, onClose }: { mod: Mod, modVer const updated = await removeModpackFromModVersion(modVersion.id, modpackVersionId); setModVersionModpacks(updated); }); - } + }; return ( diff --git a/src/components/dashboard/mods/modal/mod-versions/mod-version.tsx b/src/components/dashboard/mods/modal/mod-versions/mod-version.tsx index eee4495..a0c8260 100644 --- a/src/components/dashboard/mods/modal/mod-versions/mod-version.tsx +++ b/src/components/dashboard/mods/modal/mod-versions/mod-version.tsx @@ -23,8 +23,8 @@ export function ModVersions({ mod }: { mod: Mod }) { .catch((err) => { console.error(err); notify('Error', err.message, 'red'); - }) - }) + }); + }); const filesDrop = (files: File[]) => { startTransition(() => { @@ -39,17 +39,17 @@ export function ModVersions({ mod }: { mod: Mod }) { ); }); }); - } + }; const openModVersionModal = (modVersion?: ModVersionWithModpacks | undefined) => { setModalModVersion(modVersion); openModal(); - } + }; const closeModVersionModal = async () => { setModVersions(await getModVersionsWithModpacks(mod.id)); closeModal(); - } + }; return ( <> @@ -107,5 +107,5 @@ export function ModVersions({ mod }: { mod: Mod }) { - ) + ); } \ No newline at end of file diff --git a/src/components/dashboard/mods/modal/mods-general.tsx b/src/components/dashboard/mods/modal/mods-general.tsx index eeb4b6b..6fcf079 100644 --- a/src/components/dashboard/mods/modal/mods-general.tsx +++ b/src/components/dashboard/mods/modal/mods-general.tsx @@ -35,5 +35,5 @@ export function ModModalGeneral({ previewImg, mod, form }: ModModalGeneralProps) - ) + ); } diff --git a/src/components/dashboard/mods/modal/mods-modal.tsx b/src/components/dashboard/mods/modal/mods-modal.tsx index 450909b..fa16dfd 100644 --- a/src/components/dashboard/mods/modal/mods-modal.tsx +++ b/src/components/dashboard/mods/modal/mods-modal.tsx @@ -42,7 +42,7 @@ export function ModModal({ mod, onClose }: {mod?: Mod | undefined, onClose: (edi if (!value) return 'You must provide a name for the mod'; }, url: (value) => { - if (value && !value.startsWith('https://')) return 'The URL must start with https://' + if (value && !value.startsWith('https://')) return 'The URL must start with https://'; }, image: (value) => { if (!value) return 'You must provide an image for the modpack'; @@ -52,10 +52,10 @@ export function ModModal({ mod, onClose }: {mod?: Mod | undefined, onClose: (edi }, authors: (value) => { if (!value) return 'You must provide an author for the mod'; - } + }, }, onValuesChange: (value) => { - if (value.image && value.image instanceof File) setPreviewImg(value.image ? URL.createObjectURL(value.image) : mod?.image || '') + if (value.image && value.image instanceof File) setPreviewImg(value.image ? URL.createObjectURL(value.image) : mod?.image || ''); }, }); @@ -84,21 +84,21 @@ export function ModModal({ mod, onClose }: {mod?: Mod | undefined, onClose: (edi notify('Error', (e as Error).message, 'red'); } }); - } + }; const onDelete = (id: string) => { startTransition(async () => { await deleteMod(id); onClose(id); }); - } + }; const filesDrop = (files: File[]) => { startTransition(async () => { const data = new FormData(); files.forEach((file) => data.append('files', file)); - const addedModVersions = await addModVersionsFromJAR(data) + const addedModVersions = await addModVersionsFromJAR(data); const firstModId = addedModVersions[0].modId; const mod = await getModsFromIds([firstModId]).then((mods) => mods[0]); form.setValues({ @@ -112,7 +112,7 @@ export function ModModal({ mod, onClose }: {mod?: Mod | undefined, onClose: (edi }); setMod(mod); }); - } + }; return ( <> @@ -171,5 +171,5 @@ export function ModModal({ mod, onClose }: {mod?: Mod | undefined, onClose: (edi } - ) + ); } diff --git a/src/components/dashboard/mods/mods-panel.tsx b/src/components/dashboard/mods/mods-panel.tsx index 21d3d1e..3d9e648 100644 --- a/src/components/dashboard/mods/mods-panel.tsx +++ b/src/components/dashboard/mods/mods-panel.tsx @@ -37,7 +37,7 @@ export function ModsPanel() { console.error(err); notify('Error', err.message, 'red'); }); - } + }; const openModModal = (mod?: Mod | undefined) => { setModalMod(mod); @@ -160,5 +160,5 @@ export function ModsPanel() { } - ) + ); } \ No newline at end of file diff --git a/src/components/dashboard/progression/progression-item.tsx b/src/components/dashboard/progression/progression-item.tsx index c506390..403d78b 100644 --- a/src/components/dashboard/progression/progression-item.tsx +++ b/src/components/dashboard/progression/progression-item.tsx @@ -1,10 +1,10 @@ -import { Popover, Card, Stack, Group, Progress, Text } from '@mantine/core' +import { Popover, Card, Stack, Group, Progress, Text } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { Resolution } from '@prisma/client'; import { useDeviceSize } from '~/hooks/use-device-size'; -import { BREAKPOINT_MOBILE_LARGE, BREAKPOINT_DESKTOP_MEDIUM, BREAKPOINT_DESKTOP_LARGE } from '~/lib/constants' -import { gradient } from '~/lib/utils' +import { BREAKPOINT_MOBILE_LARGE, BREAKPOINT_DESKTOP_MEDIUM, BREAKPOINT_DESKTOP_LARGE } from '~/lib/constants'; +import { gradient } from '~/lib/utils'; import type { ModVersionWithProgression } from '~/types'; export function ProgressionItem({ modVersion }: { modVersion: ModVersionWithProgression }) { @@ -81,10 +81,10 @@ export function ProgressionItem({ modVersion }: { modVersion: ModVersionWithProg - ) + ); })} - ) + ); } \ No newline at end of file diff --git a/src/components/dashboard/progression/progression-panel.tsx b/src/components/dashboard/progression/progression-panel.tsx index 92d00b3..74f6b87 100644 --- a/src/components/dashboard/progression/progression-panel.tsx +++ b/src/components/dashboard/progression/progression-panel.tsx @@ -28,8 +28,8 @@ export function ProgressionPanel() { setResources(res.sort((a, b) => a.mod.name.localeCompare(b.mod.name))); }); getGlobalProgression() - .then(setGlobalProgress) - } + .then(setGlobalProgress); + }; return ( )} - ) + ); } \ No newline at end of file diff --git a/src/components/dashboard/users/users-panel.tsx b/src/components/dashboard/users/users-panel.tsx index 665576a..57202b7 100644 --- a/src/components/dashboard/users/users-panel.tsx +++ b/src/components/dashboard/users/users-panel.tsx @@ -33,7 +33,7 @@ export function UsersPanel() { const filtered = users.filter((user) => user.name?.toLowerCase().includes(search.toLowerCase())); setFilteredUsers(filtered.sort(sortUsers)); - } + }; useEffectOnce(() => { getUsers() diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index 771a8b4..3a03eb8 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ActionIcon, Badge, Button, Card, Combobox, Group, Image, useCombobox, } from '@mantine/core'; +import { ActionIcon, Badge, Button, Card, Combobox, Group, Image, useCombobox } from '@mantine/core'; import { UserRole } from '@prisma/client'; import Link from 'next/link'; import { usePathname, useRouter } from 'next/navigation'; @@ -29,7 +29,7 @@ export const Navbar = () => { { href: '/modpacks', label: 'Modpacks', disabled: false }, { href: '/mods', label: 'Mods', disabled: false }, { href: '/gallery', label: 'Gallery', disabled: false }, - ] + ]; if (user) links.push({ href: '/contribute', label: 'Contribute', disabled: user.role === 'BANNED' }); if (windowWidth < BREAKPOINT_TABLET) links.push({ href: '/', label: 'Home', disabled: false }); diff --git a/src/components/settings/user-panel.tsx b/src/components/settings/user-panel.tsx index 36cbea1..a9d2d93 100644 --- a/src/components/settings/user-panel.tsx +++ b/src/components/settings/user-panel.tsx @@ -29,7 +29,7 @@ export function UserSettingsPanel() { if (value.length < MIN_NAME_LENGTH) return `Your name should be at least ${MIN_NAME_LENGTH} characters long`; if (value.length > MAX_NAME_LENGTH) return `Your name should be less than ${MAX_NAME_LENGTH} characters long`; }, - } + }, }); useEffectOnce(() => { @@ -43,8 +43,8 @@ export function UserSettingsPanel() { }) .catch((err: Error) => { notify('Error', err.message, 'red'); - }) - }) + }); + }); const onSubmit = (values: typeof form.values) => { if (!displayedUser) return; @@ -57,10 +57,10 @@ export function UserSettingsPanel() { }) .catch((err) => { console.error(err); - notify('Error', err.message, 'red') + notify('Error', err.message, 'red'); }); }); - } + }; return ( - ) + ); } \ No newline at end of file diff --git a/src/components/submit/drafts/drafts-item.tsx b/src/components/submit/drafts/drafts-item.tsx index 796f302..b8fecc9 100644 --- a/src/components/submit/drafts/drafts-item.tsx +++ b/src/components/submit/drafts/drafts-item.tsx @@ -31,15 +31,15 @@ export function ContributionDraftItem({ contribution, openModal, onDelete }: Con startTransition(() => { deleteContributions(author.id!, contribution.id); onDelete(); - }) - } + }); + }; const submit = () => { startTransition(() => { submitContribution(author.id!, contribution.id); onDelete(); - }) - } + }); + }; return ( {windowWidth > BREAKPOINT_TABLET && diff --git a/src/components/theme-switch.tsx b/src/components/theme-switch.tsx index 83360f2..e33a9a9 100644 --- a/src/components/theme-switch.tsx +++ b/src/components/theme-switch.tsx @@ -22,17 +22,17 @@ export const ThemeSwitch = () => { setIcon(); break; } - } + }; const toggleColorScheme = () => { const newColorScheme = colorSchemes[(colorSchemes.indexOf(colorScheme) + 1) % colorSchemes.length]; setColorScheme(newColorScheme); setIconFromStr(newColorScheme); - } + }; useEffect(() => { setIconFromStr(colorScheme); - }, [colorScheme]) + }, [colorScheme]); return ( { {icon} ); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 055f787..e2b0a61 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,15 +1,15 @@ -import { MantineColor, MantineGradient } from '@mantine/core' -import { showNotification } from '@mantine/notifications' -import { Resolution } from '@prisma/client' -import { type ClassValue, clsx } from 'clsx' -import { twMerge } from 'tailwind-merge' +import { MantineColor, MantineGradient } from '@mantine/core'; +import { showNotification } from '@mantine/notifications'; +import { Resolution } from '@prisma/client'; +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; -import { Progression } from '~/types' +import { Progression } from '~/types'; -import { NOTIFICATIONS_DURATION_MS } from './constants' +import { NOTIFICATIONS_DURATION_MS } from './constants'; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } export const gradient: MantineGradient = { @@ -25,10 +25,10 @@ export const gradientDanger: MantineGradient = { }; export function capitalize(str: string) { - if (str.length === 0) return str + if (str.length === 0) return str; - const words = str.split(' ') - return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLocaleLowerCase()).join(' ') + const words = str.split(' '); + return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLocaleLowerCase()).join(' '); } export function notify(title: string, message: React.ReactNode, color: MantineColor) { diff --git a/src/middleware.ts b/src/middleware.ts index ea36aea..b33f476 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -21,9 +21,9 @@ export default auth((req) => { return Response.redirect(new URL('/not-found', nextUrl)); return; -}) +}); // Optionally, don't invoke Middleware on some paths export const config = { matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'], -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/server/actions/files.ts b/src/server/actions/files.ts index af2df02..17c85df 100644 --- a/src/server/actions/files.ts +++ b/src/server/actions/files.ts @@ -89,7 +89,7 @@ function sanitizeMCModInfo(mcmodInfo: MCModInfoData): MCModInfo[] { if (!modInfo.version) modInfo.name = 'unknown'; return modInfo; - }) + }); } /** @@ -157,7 +157,7 @@ export async function extractDefaultResourcePack(jar: File, modVersion: ModVersi file.path.includes('textures') && (file.path.endsWith('.png') || file.path.endsWith('.mcmeta')) && !file.path.endsWith('/') - ) + ); // TODO: Get models assets diff --git a/src/server/data/mods-version.ts b/src/server/data/mods-version.ts index 2630d3f..5e8b674 100644 --- a/src/server/data/mods-version.ts +++ b/src/server/data/mods-version.ts @@ -37,8 +37,8 @@ export async function getModsVersionsProgression(): Promise ({ ...resource, ...EMPTY_PROGRESSION, - })) - })) + })), + })); for (const modVersion of modVersions) { for (const resource of modVersion.resources) { @@ -46,7 +46,7 @@ export async function getModsVersionsProgression(): Promise lt.textureId) } + id: { in: linkedTextures.map((lt) => lt.textureId) }, }, }); @@ -70,7 +70,7 @@ export async function getModsVersionsProgression(): Promise { - const modpackVersionId = await db.modpackVersion.findFirst({ where: { modpackId, mods: { some: { id: modVersionId } }}}) + const modpackVersionId = await db.modpackVersion.findFirst({ where: { modpackId, mods: { some: { id: modVersionId } }}}); if (!modpackVersionId) throw new Error(`Modpack with id '${modpackId}' not found`); await removeModFromModpackVersion(modpackVersionId.id, modVersionId); diff --git a/src/server/data/texture.ts b/src/server/data/texture.ts index 1cbda30..5cde370 100644 --- a/src/server/data/texture.ts +++ b/src/server/data/texture.ts @@ -17,8 +17,8 @@ export async function createTexture({ name, filepath, hash }: { name: string, fi name, filepath, hash, - } - }) + }, + }); } export async function getGlobalProgression() { @@ -41,12 +41,12 @@ export async function getGlobalProgression() { } return output; - }) + }); return { linkedTextures, textures: { done: contributedTextures, todo }, - } + }; } export async function findTexture({ From f8ead00b0d4f5c62f4c0ab35773166955fff7624 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Sun, 7 Apr 2024 22:32:40 +0200 Subject: [PATCH 21/26] fix: SCSS linting --- package.json | 2 +- src/app/globals.scss | 22 ++++------------------ src/app/mantine.scss | 18 ++++++++++++++++++ src/components/submit/submit.scss | 7 ++++--- 4 files changed, 27 insertions(+), 22 deletions(-) create mode 100644 src/app/mantine.scss diff --git a/package.json b/package.json index 8567a6c..99da119 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint --fix", + "lint": "next lint --fix; npx stylelint \"src/**/*.{scss,css}\" --fix", "postinstall": "prisma generate" }, "dependencies": { diff --git a/src/app/globals.scss b/src/app/globals.scss index c39ac36..4c58844 100644 --- a/src/app/globals.scss +++ b/src/app/globals.scss @@ -1,3 +1,4 @@ +@import './mantine'; @tailwind components; @tailwind utilities; @@ -29,6 +30,7 @@ main { .texture-background { background-color: var(--mantine-color-dark-6); } + :where([data-mantine-color-scheme='light']) .texture-background { background-color: var(--mantine-color-gray-1); } @@ -36,6 +38,7 @@ main { .image-background { background-color: var(--mantine-color-dark-7); } + :where([data-mantine-color-scheme='light']) .image-background { background-color: var(--mantine-primary-color-1); } @@ -72,23 +75,6 @@ main { :where([data-mantine-color-scheme='dark']) .button-disabled-with-bg { &:disabled, &[data-disabled] { - background-color: rgba(0, 0, 0, 0.075); + background-color: rgb(0 0 0 / 7.5%); } } - -.mantine-Modal-inner { - left: 0; -} - -:where([data-mantine-color-scheme='dark']) .mantine-Accordion-item, -.mantine-Accordion-item { - background-color: var(--item-filled-color) !important; - box-shadow: var(--mantine-shadow-sm); - border: calc(0.0625rem * var(--mantine-scale)) solid; - border-radius: var(--accordion-radius); - border-color: var(--item-border-color); -} - -:where([data-mantine-color-scheme='light']) .mantine-Accordion-item { - background-color: var(--mantine-color-white) !important; -} \ No newline at end of file diff --git a/src/app/mantine.scss b/src/app/mantine.scss new file mode 100644 index 0000000..87811e3 --- /dev/null +++ b/src/app/mantine.scss @@ -0,0 +1,18 @@ +/* stylelint-disable selector-class-pattern -- mantine class naming convention is against */ + +.mantine-Modal-inner { + left: 0; +} + +:where([data-mantine-color-scheme='dark']) .mantine-Accordion-item, +.mantine-Accordion-item { + background-color: var(--item-filled-color) !important; + box-shadow: var(--mantine-shadow-sm); + border: calc(0.0625rem * var(--mantine-scale)) solid; + border-radius: var(--accordion-radius); + border-color: var(--item-border-color); +} + +:where([data-mantine-color-scheme='light']) .mantine-Accordion-item { + background-color: var(--mantine-color-white) !important; +} \ No newline at end of file diff --git a/src/components/submit/submit.scss b/src/components/submit/submit.scss index 75d6910..e994a60 100644 --- a/src/components/submit/submit.scss +++ b/src/components/submit/submit.scss @@ -13,6 +13,7 @@ .danger-border { --border-size: 5px; + position: relative; &::before { @@ -26,11 +27,11 @@ background: linear-gradient( 60deg, #ffa500, - #ff0000, + #f00, ); background-size: 300% 300%; background-position: 0 50%; - animation: moveGradient 4s alternate infinite; + animation: move-gradient 4s alternate infinite; border-radius: var(--mantine-radius-sm); } @@ -56,7 +57,7 @@ background-color: var(--mantine-color-dark-6); } -@keyframes moveGradient { +@keyframes move-gradient { 50% { background-position: 100% 50%; } From eecdf399ed1d2cfd5f46e9cefa76f676b349dd2b Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Sun, 7 Apr 2024 22:44:42 +0200 Subject: [PATCH 22/26] feat #2 : add loading state when auto selecting a texture --- src/components/submit/drafts/drafts-modal.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/submit/drafts/drafts-modal.tsx b/src/components/submit/drafts/drafts-modal.tsx index 4910ea7..d1ecb1f 100644 --- a/src/components/submit/drafts/drafts-modal.tsx +++ b/src/components/submit/drafts/drafts-modal.tsx @@ -154,12 +154,15 @@ export function ContributionDraftModal({ contribution, textures, onClose }: Cont variant="light" color={gradient.to} className="navbar-icon-fix" + loading={isPending} onClick={() => { - // TODO #18 look for aliases - const texture = textures.find((t) => sanitizeTextureName(t.name) === contribution.filename.replace('.png', '')); - if (texture) { - selectedTextureUpdated(texture.id); - } + startTransition(() => { + // TODO #18 look for aliases + const texture = textures.find((t) => sanitizeTextureName(t.name) === contribution.filename.replace('.png', '')); + if (texture) { + selectedTextureUpdated(texture.id); + } + }); }} > From 64c7dd06af3a81e397b771626cf3447c2378a8c5 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Sun, 7 Apr 2024 22:53:57 +0200 Subject: [PATCH 23/26] feat #2 : correctly update contribute page on change --- .../(pages)/(protected)/contribute/page.tsx | 37 ++++++++++--------- .../submit/submitted/submitted-panel.tsx | 19 +++++----- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/app/(pages)/(protected)/contribute/page.tsx b/src/app/(pages)/(protected)/contribute/page.tsx index 1f6532d..ae98b0e 100644 --- a/src/app/(pages)/(protected)/contribute/page.tsx +++ b/src/app/(pages)/(protected)/contribute/page.tsx @@ -31,25 +31,24 @@ const ContributePage = () => { const user = useCurrentUser()!; // the user is guaranteed to be logged in (per the layout) useEffectOnce(() => { - getDraftContributions(user.id!) - .then(setDraftContributions) - .catch((err) => { - console.error(err); - notify('Error', 'Failed to fetch draft contributions', 'red'); - }); - - getCoSubmittedContributions(user.id!) - .then(setCoContributions) - .catch((err) => { - console.error(err); - notify('Error', 'Failed to fetch submitted contributions', 'red'); - }); - - reloadSubmitted(); + reload(); }); - - const reloadSubmitted = () => { + + const reload = () => { startTransition(() => { + getDraftContributions(user.id!) + .then(setDraftContributions) + .catch((err) => { + console.error(err); + notify('Error', 'Failed to fetch draft contributions', 'red'); + }); + + getCoSubmittedContributions(user.id!) + .then(setCoContributions) + .catch((err) => { + console.error(err); + notify('Error', 'Failed to fetch submitted contributions', 'red'); + }); getSubmittedContributions(user.id!) .then(setContributions) .catch((err) => { @@ -154,7 +153,7 @@ const ContributePage = () => { c.id).join('')} - onUpdate={reloadSubmitted} + onUpdate={reload} /> @@ -172,6 +171,7 @@ const ContributePage = () => { c.id).join('')} + onUpdate={reload} /> @@ -190,6 +190,7 @@ const ContributePage = () => { coSubmitted contributions={coContributions} key={coContributions.map((c) => c.id).join('')} + onUpdate={reload} /> diff --git a/src/components/submit/submitted/submitted-panel.tsx b/src/components/submit/submitted/submitted-panel.tsx index a698394..994122c 100644 --- a/src/components/submit/submitted/submitted-panel.tsx +++ b/src/components/submit/submitted/submitted-panel.tsx @@ -19,13 +19,13 @@ import '../submit.scss'; export interface ContributionDraftPanelProps { contributions: ContributionWithCoAuthorsAndPoll[]; coSubmitted?: boolean; + onUpdate: () => void; } -export function ContributionSubmittedPanel({ contributions, coSubmitted }: ContributionDraftPanelProps) { +export function ContributionSubmittedPanel({ contributions, coSubmitted, onUpdate }: ContributionDraftPanelProps) { const [windowWidth, _] = useDeviceSize(); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [isPending, startTransition] = useTransition(); - const [displayedContributions, setDisplayedContributions] = useState(contributions); const [searchedContributions, setSearchedContributions] = useState(contributions); const [deletionMode, setDeletionMode] = useState(false); @@ -54,24 +54,23 @@ export function ContributionSubmittedPanel({ contributions, coSubmitted }: Contr const deleteDeletionList = () => { if (deletionList.length === 0) return; + startTransition(() => { deleteContributions(author.id!, ...deletionList); closeModal(); - setDisplayedContributions(displayedContributions.filter((c) => !deletionList.includes(c.id))); - setDeletionList([]); - searchContribution(form.values.search); // update displayed contributions + onUpdate(); }); }; const searchContribution = (search: string) => { - if (!displayedContributions) return; + if (!contributions) return; if (!search || search.length === 0) { - setSearchedContributions(displayedContributions); + setSearchedContributions(contributions); return; } - const filtered = displayedContributions?.filter((c) => c.filename.toLowerCase().includes(search.toLowerCase())); + const filtered = contributions.filter((c) => c.filename.toLowerCase().includes(search.toLowerCase())); setSearchedContributions(filtered); }; @@ -166,8 +165,8 @@ export function ContributionSubmittedPanel({ contributions, coSubmitted }: Contr - {displayedContributions && displayedContributions.length === 0 && Nothing yet} - {displayedContributions && displayedContributions.length > 0 && + {contributions.length === 0 && Nothing yet} + {contributions.length > 0 && <> {searchedContributions.length > 0 && searchedContributions.map((contribution, index) => From d9efee60f5cd642f761b784456314c32373002cc Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Sun, 7 Apr 2024 22:58:34 +0200 Subject: [PATCH 24/26] feat #2: rename comp directory from submit to contribute --- src/components/{submit => contribute}/co-authors-select.tsx | 0 src/components/{submit => contribute}/drafts/drafts-item.tsx | 0 src/components/{submit => contribute}/drafts/drafts-modal.tsx | 0 src/components/{submit => contribute}/drafts/drafts-panel.tsx | 0 src/components/{submit => contribute}/submit.scss | 0 .../{submit => contribute}/submitted/submitted-item.tsx | 0 .../{submit => contribute}/submitted/submitted-panel.tsx | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename src/components/{submit => contribute}/co-authors-select.tsx (100%) rename src/components/{submit => contribute}/drafts/drafts-item.tsx (100%) rename src/components/{submit => contribute}/drafts/drafts-modal.tsx (100%) rename src/components/{submit => contribute}/drafts/drafts-panel.tsx (100%) rename src/components/{submit => contribute}/submit.scss (100%) rename src/components/{submit => contribute}/submitted/submitted-item.tsx (100%) rename src/components/{submit => contribute}/submitted/submitted-panel.tsx (100%) diff --git a/src/components/submit/co-authors-select.tsx b/src/components/contribute/co-authors-select.tsx similarity index 100% rename from src/components/submit/co-authors-select.tsx rename to src/components/contribute/co-authors-select.tsx diff --git a/src/components/submit/drafts/drafts-item.tsx b/src/components/contribute/drafts/drafts-item.tsx similarity index 100% rename from src/components/submit/drafts/drafts-item.tsx rename to src/components/contribute/drafts/drafts-item.tsx diff --git a/src/components/submit/drafts/drafts-modal.tsx b/src/components/contribute/drafts/drafts-modal.tsx similarity index 100% rename from src/components/submit/drafts/drafts-modal.tsx rename to src/components/contribute/drafts/drafts-modal.tsx diff --git a/src/components/submit/drafts/drafts-panel.tsx b/src/components/contribute/drafts/drafts-panel.tsx similarity index 100% rename from src/components/submit/drafts/drafts-panel.tsx rename to src/components/contribute/drafts/drafts-panel.tsx diff --git a/src/components/submit/submit.scss b/src/components/contribute/submit.scss similarity index 100% rename from src/components/submit/submit.scss rename to src/components/contribute/submit.scss diff --git a/src/components/submit/submitted/submitted-item.tsx b/src/components/contribute/submitted/submitted-item.tsx similarity index 100% rename from src/components/submit/submitted/submitted-item.tsx rename to src/components/contribute/submitted/submitted-item.tsx diff --git a/src/components/submit/submitted/submitted-panel.tsx b/src/components/contribute/submitted/submitted-panel.tsx similarity index 100% rename from src/components/submit/submitted/submitted-panel.tsx rename to src/components/contribute/submitted/submitted-panel.tsx From 0abbb79841e29cd43e94d4946bc25ce2c3ee6e6c Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Sun, 7 Apr 2024 23:02:02 +0200 Subject: [PATCH 25/26] fix : add ESLint plugin to remove unused imports --- .eslintrc.json | 2 ++ package-lock.json | 31 +++++++++++++++++++ package.json | 1 + .../(pages)/(protected)/contribute/page.tsx | 11 +++---- .../contribute/drafts/drafts-modal.tsx | 2 +- .../contributions/contributions-panel.tsx | 2 +- .../modpack-version-modal.tsx | 4 +-- .../dashboard/modpacks/modpacks-panel.tsx | 2 +- src/components/dashboard/mods/mods-panel.tsx | 2 +- src/server/data/mods-version.ts | 4 +-- src/types/index.d.ts | 2 -- 11 files changed, 47 insertions(+), 16 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 7a2f9b6..961d5b3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,10 +1,12 @@ { "extends": "next/core-web-vitals", + "plugins": ["unused-imports"], "rules": { "indent": ["error", "tab"], "quotes": ["error", "single"], "semi": ["error", "always"], "comma-dangle": ["error", "always-multiline"], + "unused-imports/no-unused-imports": "error", "import/order": [ "error", { diff --git a/package-lock.json b/package-lock.json index f67d1b7..cb3dafd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,6 +66,7 @@ "autoprefixer": "^10.0.1", "eslint": "^8", "eslint-config-next": "14.0.4", + "eslint-plugin-unused-imports": "^3.1.0", "postcss": "^8.4.35", "postcss-preset-mantine": "^1.13.0", "postcss-simple-vars": "^7.0.1", @@ -4353,6 +4354,36 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-plugin-unused-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.1.0.tgz", + "integrity": "sha512-9l1YFCzXKkw1qtAru1RWUtG2EVDZY0a0eChKXcL+EZ5jitG7qxdctu4RnvhOJHv4xfmUf7h+JJPINlVpGhZMrw==", + "dev": true, + "dependencies": { + "eslint-rule-composer": "^0.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "6 - 7", + "eslint": "8" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", diff --git a/package.json b/package.json index 99da119..fb7229d 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "autoprefixer": "^10.0.1", "eslint": "^8", "eslint-config-next": "14.0.4", + "eslint-plugin-unused-imports": "^3.1.0", "postcss": "^8.4.35", "postcss-preset-mantine": "^1.13.0", "postcss-simple-vars": "^7.0.1", diff --git a/src/app/(pages)/(protected)/contribute/page.tsx b/src/app/(pages)/(protected)/contribute/page.tsx index ae98b0e..c654858 100644 --- a/src/app/(pages)/(protected)/contribute/page.tsx +++ b/src/app/(pages)/(protected)/contribute/page.tsx @@ -1,20 +1,19 @@ 'use client'; -import { Accordion, Badge, Card, Code, Group, Select, Stack, Text, Title } from '@mantine/core'; +import { Accordion, Badge, Card, Code, Group, Select, Stack, Text } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { Resolution } from '@prisma/client'; -import Link from 'next/link'; import { useState, useTransition } from 'react'; -import { CoAuthorsSelector } from '~/components/submit/co-authors-select'; -import { ContributionDraftPanel } from '~/components/submit/drafts/drafts-panel'; -import { ContributionSubmittedPanel } from '~/components/submit/submitted/submitted-panel'; +import { CoAuthorsSelector } from '~/components/contribute/co-authors-select'; +import { ContributionDraftPanel } from '~/components/contribute/drafts/drafts-panel'; +import { ContributionSubmittedPanel } from '~/components/contribute/submitted/submitted-panel'; import { useCurrentUser } from '~/hooks/use-current-user'; import { useDeviceSize } from '~/hooks/use-device-size'; import { useEffectOnce } from '~/hooks/use-effect-once'; import { BREAKPOINT_MOBILE_LARGE } from '~/lib/constants'; import { gradient, notify } from '~/lib/utils'; -import { createRawContributions, getSubmittedContributions, getDraftContributions, getCoSubmittedContributions } from '~/server/data/contributions'; +import { createRawContributions, getCoSubmittedContributions, getDraftContributions, getSubmittedContributions } from '~/server/data/contributions'; import type { ContributionWithCoAuthors, ContributionWithCoAuthorsAndPoll, PublicUser } from '~/types'; const ContributePage = () => { diff --git a/src/components/contribute/drafts/drafts-modal.tsx b/src/components/contribute/drafts/drafts-modal.tsx index d1ecb1f..b0f6dea 100644 --- a/src/components/contribute/drafts/drafts-modal.tsx +++ b/src/components/contribute/drafts/drafts-modal.tsx @@ -7,7 +7,7 @@ import { PiMagicWandBold } from 'react-icons/pi'; import { useCurrentUser } from '~/hooks/use-current-user'; import { useDeviceSize } from '~/hooks/use-device-size'; import { useEffectOnce } from '~/hooks/use-effect-once'; -import { BREAKPOINT_DESKTOP_MEDIUM, BREAKPOINT_MOBILE_LARGE, BREAKPOINT_TABLET } from '~/lib/constants'; +import { BREAKPOINT_MOBILE_LARGE } from '~/lib/constants'; import { gradient, gradientDanger } from '~/lib/utils'; import { getContributionsOfTexture, updateDraftContribution } from '~/server/data/contributions'; import type { ContributionWithCoAuthors, ContributionWithCoAuthorsAndPoll, PublicUser } from '~/types'; diff --git a/src/components/council/contributions/contributions-panel.tsx b/src/components/council/contributions/contributions-panel.tsx index 1eeea79..0a1b1b3 100644 --- a/src/components/council/contributions/contributions-panel.tsx +++ b/src/components/council/contributions/contributions-panel.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Accordion, Badge, Card, Group, Stack, Text, Title } from '@mantine/core'; +import { Accordion, Badge, Card, Stack, Text } from '@mantine/core'; import { Texture } from '@prisma/client'; import { useState } from 'react'; diff --git a/src/components/dashboard/modpacks/modal/modpack-versions/modpack-version-modal.tsx b/src/components/dashboard/modpacks/modal/modpack-versions/modpack-version-modal.tsx index b503072..9a55ae5 100644 --- a/src/components/dashboard/modpacks/modal/modpack-versions/modpack-version-modal.tsx +++ b/src/components/dashboard/modpacks/modal/modpack-versions/modpack-version-modal.tsx @@ -1,9 +1,9 @@ 'use client'; -import { Button, Code, Group, Stack, Text, TextInput, rem } from '@mantine/core'; +import { Button, Code, Group, Stack, Text, TextInput } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { useForm } from '@mantine/form'; -import { Mod, ModVersion, Modpack, ModpackVersion } from '@prisma/client'; +import { Mod, ModVersion, Modpack } from '@prisma/client'; import { useState, useTransition } from 'react'; import { useEffectOnce } from '~/hooks/use-effect-once'; diff --git a/src/components/dashboard/modpacks/modpacks-panel.tsx b/src/components/dashboard/modpacks/modpacks-panel.tsx index d98b6d6..40cd59f 100644 --- a/src/components/dashboard/modpacks/modpacks-panel.tsx +++ b/src/components/dashboard/modpacks/modpacks-panel.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Image, Badge, Card, Group, Text, TextInput, Button, Stack, Modal } from '@mantine/core'; +import { Badge, Card, Group, Text, TextInput, Button, Modal } from '@mantine/core'; import { useForm } from '@mantine/form'; import { useDisclosure } from '@mantine/hooks'; import { Modpack } from '@prisma/client'; diff --git a/src/components/dashboard/mods/mods-panel.tsx b/src/components/dashboard/mods/mods-panel.tsx index 3d9e648..f0701c8 100644 --- a/src/components/dashboard/mods/mods-panel.tsx +++ b/src/components/dashboard/mods/mods-panel.tsx @@ -1,6 +1,6 @@ import type { Mod } from '@prisma/client'; -import { Badge, Button, Card, Group, Image, Modal, Stack, Text, TextInput } from '@mantine/core'; +import { Badge, Button, Card, Group, Modal, Text, TextInput } from '@mantine/core'; import { useForm } from '@mantine/form'; import { useDisclosure } from '@mantine/hooks'; import { useState, useTransition } from 'react'; diff --git a/src/server/data/mods-version.ts b/src/server/data/mods-version.ts index 5e8b674..f52a43e 100644 --- a/src/server/data/mods-version.ts +++ b/src/server/data/mods-version.ts @@ -1,11 +1,11 @@ 'use server'; -import { Resolution, type ModVersion, type Modpack, $Enums } from '@prisma/client'; +import { Resolution, type ModVersion, type Modpack } from '@prisma/client'; import { canAccess } from '~/lib/auth'; import { db } from '~/lib/db'; import { EMPTY_PROGRESSION, EMPTY_PROGRESSION_RES } from '~/lib/utils'; -import type { ModVersionWithModpacks, ModVersionWithProgression, Progression } from '~/types'; +import type { ModVersionWithModpacks, ModVersionWithProgression } from '~/types'; import { removeModFromModpackVersion } from './modpacks-version'; import { deleteResource } from './resource'; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 11c2234..9f0c256 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,6 +1,5 @@ import type { Contribution, - LinkedTexture, Mod, Modpack, ModpackVersion, @@ -8,7 +7,6 @@ import type { Poll, Resolution, Resource, - Texture, } from '@prisma/client'; export type Prettify = { From 7d4a0e21a93db123e8aeaca154605a8b995f6f08 Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Sun, 7 Apr 2024 23:10:23 +0200 Subject: [PATCH 26/26] tools : add more configs to .editorconfig --- .editorconfig | 4 ++++ .vscode/extensions.json | 5 +++++ 2 files changed, 9 insertions(+) create mode 100644 .vscode/extensions.json diff --git a/.editorconfig b/.editorconfig index 1cf5ec5..d2d0a38 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,7 +1,11 @@ [*] +charset = utf-8 +end_of_line = lf tab_width = 2 indent_style = tab quote_type = single +insert_final_newline = true +trim_trailing_whitespace = true [*.yml] tab_width = 2 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..869a89a --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "editorconfig.editorconfig" + ] +} \ No newline at end of file