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/.eslintrc.json b/.eslintrc.json index e7e1882..961d5b3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,8 +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/.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 diff --git a/.vscode/settings.json b/.vscode/settings.json index 4cce1b9..0c79879 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,9 @@ { "cSpell.words": [ "dependants", + "downvoted", + "Downvoted", + "downvotes", "dropzone", "hookform", "mantine", @@ -11,7 +14,12 @@ "prisma", "stylelint", "tailwindcss", - "unzipper" + "unvoted", + "Unvoted", + "unzipper", + "upvoted", + "Upvoted", + "upvotes" ], "files.exclude": { "**/.git": true, diff --git a/package-lock.json b/package-lock.json index 1624048..cb3dafd 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", @@ -66,10 +66,11 @@ "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", - "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 +1217,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 +1234,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": { @@ -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", @@ -7391,13 +7422,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..fb7229d 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": { @@ -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", @@ -67,10 +67,11 @@ "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", - "prisma": "^5.10.2", + "prisma": "^5.12.1", "stylelint": "^15.11.0", "stylelint-config-standard-scss": "^8.0.0", "stylelint-scss": "^4.7.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 04bac75..f3336fb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,6 +10,7 @@ generator client { enum UserRole { ADMIN + COUNCIL USER BANNED } @@ -20,6 +21,7 @@ enum Resolution { } enum Status { + DRAFT PENDING ACCEPTED REJECTED @@ -67,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") @@ -99,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") @@ -183,14 +185,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("contributions_to_coauthors") + 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]) @@ -205,36 +212,38 @@ model Contribution { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - @@map("users_contributions") + @@map("contributions") } 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[] + + 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[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + Contribution Contribution? - @@map("users_polls") + @@map("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..c654858 --- /dev/null +++ b/src/app/(pages)/(protected)/contribute/page.tsx @@ -0,0 +1,202 @@ +'use client'; + +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'; + +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, getCoSubmittedContributions, getDraftContributions, getSubmittedContributions } from '~/server/data/contributions'; +import type { ContributionWithCoAuthors, ContributionWithCoAuthorsAndPoll, PublicUser } from '~/types'; + +const ContributePage = () => { + const [isPending, startTransition] = useTransition(); + const [windowWidth, _] = useDeviceSize(); + + const [resolution, setResolution] = useState(Resolution.x32); + const [selectedCoAuthors, setSelectedCoAuthors] = useState([]); + + 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) + + useEffectOnce(() => { + reload(); + }); + + 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) => { + console.error(err); + notify('Error', 'Failed to fetch contributions', 'red'); + }); + }); + }; + + const filesDrop = (files: File[]) => { + startTransition(async () => { + const data = new FormData(); + files.forEach((file) => data.append('files', file)); + + await createRawContributions(user.id!, selectedCoAuthors.map((u) => u.id), resolution, data); + getDraftContributions(user.id!).then(setDraftContributions); + }); + }; + + 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). +
+ + + + + New contribution(s) + + Please do not submit textures for unsupported mod/modpack. Ask the admins to add it first. + + ({ value: t.id, label: sanitizeTextureName(t.name), disabled: t.id === selectedTexture?.id }))} + defaultValue={contribution.textureId} + renderOption={renderMultiSelectOption} + className="w-full" + onChange={selectedTextureUpdated} + onClear={() => setSelectedTexture(null)} + searchValue={sanitizeTextureName(selectedTexture?.name ?? '')} + placeholder="Search a texture..." + searchable + clearable + /> + + {selectedTexture && + + } + {!selectedTexture && } + + {/* Existing contribution */} + + + Existing Contributions + For 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 + + +