From 61e52b0e69c96282030351ce3bef1ab61eb3224b Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Fri, 16 Aug 2024 04:26:34 +0200 Subject: [PATCH 01/30] version : bump to 1.0.9 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c16cb75a..342f0faa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "faithful-mods", - "version": "1.0.8", + "version": "1.0.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "faithful-mods", - "version": "1.0.8", + "version": "1.0.9", "hasInstallScript": true, "dependencies": { "@auth/prisma-adapter": "^2.4.2", diff --git a/package.json b/package.json index 12642b58..6b624aea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "faithful-mods", - "version": "1.0.8", + "version": "1.0.9", "private": true, "scripts": { "dev": "npx cross-env NODE_ENV=development tsx watch src/server.ts", From a5ca8313624936a665237b8b0e68fbcd96c0a349 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Fri, 16 Aug 2024 04:31:36 +0200 Subject: [PATCH 02/30] fix : remove contribution owner from co-authors #132 --- src/server/actions/faithful-pack.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/actions/faithful-pack.ts b/src/server/actions/faithful-pack.ts index 03b69b2e..053ad25c 100644 --- a/src/server/actions/faithful-pack.ts +++ b/src/server/actions/faithful-pack.ts @@ -110,6 +110,7 @@ export async function updateCachedFP(): Promise<void> { })(), coAuthors: c.authors + .slice(1) .map((id) => users.find((u) => u.id === id)) .filter((u) => !!u) .map((u) => ({ From fc55703f8658ff53b9abe17873a6883da6e7b3bd Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Sat, 17 Aug 2024 02:51:50 +0200 Subject: [PATCH 03/30] feat : add default mod texture to the git repo --- package-lock.json | 166 ++++++++++++++++++++++++++++++++++++ package.json | 1 + src/auth.config.ts | 5 ++ src/lib/constants.ts | 5 +- src/server/actions/files.ts | 31 +++++-- src/server/actions/git.ts | 122 ++++++++++++++++++++++++++ src/types/index.d.ts | 2 + 7 files changed, 323 insertions(+), 9 deletions(-) create mode 100644 src/server/actions/git.ts diff --git a/package-lock.json b/package-lock.json index 342f0faa..82fc62eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@mantine/hooks": "^7.12.0", "@mantine/modals": "^7.12.0", "@mantine/notifications": "^7.12.0", + "@octokit/rest": "^21.0.2", "@types/unzipper": "^0.10.9", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", @@ -1320,6 +1321,159 @@ "node": ">= 8" } }, + "node_modules/@octokit/auth-token": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", + "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", + "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.0.0", + "@octokit/request": "^9.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", + "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", + "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^9.0.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.3.tgz", + "integrity": "sha512-o4WRoOJZlKqEEgj+i9CpcmnByvtzoUYC6I8PD2SA95M+BJ2x8h7oLcVOg9qcowWXBOdcTRsMZiwvM3EyLm9AfA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.5.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz", + "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.4.tgz", + "integrity": "sha512-gusyAVgTrPiuXOdfqOySMDztQHv6928PQ3E4dqVGEtOvRXAKRbJR4b1zQyniIT9waqaWk/UDaoJ2dyPr7Bk7Iw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.5.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/request": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", + "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^10.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.4.tgz", + "integrity": "sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest": { + "version": "21.0.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.0.2.tgz", + "integrity": "sha512-+CiLisCoyWmYicH25y1cDfCrv41kRSvTq6pPWtRroRJzhsCZWZyCqGyI8foJT5LmScADSwRAnr/xo+eewL04wQ==", + "license": "MIT", + "dependencies": { + "@octokit/core": "^6.1.2", + "@octokit/plugin-paginate-rest": "^11.0.0", + "@octokit/plugin-request-log": "^5.3.1", + "@octokit/plugin-rest-endpoint-methods": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", + "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, "node_modules/@panva/hkdf": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.1.1.tgz", @@ -2000,6 +2154,12 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/before-after-hook": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", + "license": "Apache-2.0" + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -7234,6 +7394,12 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/universal-user-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", + "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", + "license": "ISC" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", diff --git a/package.json b/package.json index 6b624aea..9c4895ad 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@mantine/hooks": "^7.12.0", "@mantine/modals": "^7.12.0", "@mantine/notifications": "^7.12.0", + "@octokit/rest": "^21.0.2", "@types/unzipper": "^0.10.9", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", diff --git a/src/auth.config.ts b/src/auth.config.ts index 96f4112f..7d85b1a2 100644 --- a/src/auth.config.ts +++ b/src/auth.config.ts @@ -10,6 +10,11 @@ export default { Github({ clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, + authorization: { + params: { + scope: 'read:user user:email public_repo', + }, + }, }), ], } satisfies NextAuthConfig; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index d0511810..220b90b0 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -105,5 +105,6 @@ export const COLORS: Record<Status, MantineColor> = { [Status.ARCHIVED]: 'gainsboro', }; -// export const DRAFT_COLOR: MantineColor = 'gray'; -// export const PENDING +export const GITHUB_ORG_NAME = 'faithful-mods'; +export const GITHUB_DEFAULT_REPO_NAME = 'resources-default'; +export const FILE_GIT = `https://raw.githubusercontent.com/${GITHUB_ORG_NAME}/${GITHUB_DEFAULT_REPO_NAME}/${process.env.NODE_ENV === 'production' ? 'main' : 'dev'}`; diff --git a/src/server/actions/files.ts b/src/server/actions/files.ts index e1750149..83650953 100644 --- a/src/server/actions/files.ts +++ b/src/server/actions/files.ts @@ -8,12 +8,13 @@ import { join } from 'path'; import TOML from '@ltd/j-toml'; import unzipper from 'unzipper'; -import { FILE_DIR, FILE_PATH } from '~/lib/constants'; +import { FILE_DIR, FILE_GIT, FILE_PATH } from '~/lib/constants'; import { db } from '~/lib/db'; import { calculateHash } from '~/lib/hash'; import { socket } from '~/lib/serversocket'; -import { bufferToFile, sortBySemver } from '~/lib/utils'; +import { sortBySemver } from '~/lib/utils'; +import { uploadToRepository } from './git'; import { createMod, updateModPicture } from '../data/mods'; import { createModVersion } from '../data/mods-version'; import { linkTextureToResource, createResource, getResource } from '../data/resource'; @@ -21,7 +22,7 @@ import { createTexture, findTexture } from '../data/texture'; import type { ModVersion } from '@prisma/client'; import type { CentralDirectory } from 'unzipper'; -import type { MCModInfoData, ModData, ModFabricJson, ModFabricJsonPerson, ModsToml, SocketModUpload } from '~/types'; +import type { base64, MCModInfoData, ModData, ModFabricJson, ModFabricJsonPerson, ModsToml, SocketModUpload } from '~/types'; /** * Uploads a file to the server @@ -245,7 +246,6 @@ export async function extractModVersionsFromJAR(jar: File, socketId: string, sta }); } - console.log(modInfo.picture, mod.image); if (modInfo.picture && !mod.image) { const formData = new FormData(); formData.append('file', new Blob([modInfo.picture]), 'picture.png'); @@ -305,7 +305,10 @@ export async function extractDefaultResourcePack(jar: File, modVersion: ModVersi const fileDirPrv = join(FILE_PATH, 'textures', 'default'); if (!existsSync(fileDirPrv)) mkdirSync(fileDirPrv, { recursive: true }); - // Check if the extracted file already exists in the public dir + const filesToCommit: base64[] = []; + const filesNamesOnGit: string[] = []; + + // Check if the extracted file already exists in the database for (const textureAsset of textureAssets) { const textureName = textureAsset.path.split('/').pop()!.split('.')[0] ?? 'unknown'; const asset = textureAsset.path.split('/')[1] ?? 'unknown'; @@ -316,7 +319,6 @@ export async function extractDefaultResourcePack(jar: File, modVersion: ModVersi let texture = await findTexture({ hash }); if (!texture) { - const filepath = await upload(bufferToFile(buffer, `${textureName}.png`, 'image/png'), 'textures/default/'); const mcmetaFile = mcmetaAssets.find((mcmeta) => `${textureAsset.path}.mcmeta` === mcmeta.path); let mcmeta = undefined; @@ -337,11 +339,17 @@ export async function extractDefaultResourcePack(jar: File, modVersion: ModVersi } texture = await createTexture({ - filepath, + filepath: '', hash, name: textureName, mcmeta, }); + + const filepath = `${FILE_GIT}/${texture.id}.png` as const; + await db.texture.update({ where: { id: texture.id }, data: { filepath } }); + + filesToCommit.push(buffer.toString('base64') as base64); + filesNamesOnGit.push(`${texture.id}.png`); } else { if (texture.name !== textureName && !texture.aliases.includes(textureName)) { @@ -360,5 +368,14 @@ export async function extractDefaultResourcePack(jar: File, modVersion: ModVersi socket?.emit(socketId, status); } + const mod = await db.mod.findFirstOrThrow({ where: { id: modVersion.modId } }); + + // push new files to git + await uploadToRepository( + filesToCommit, + filesNamesOnGit, + `textures: add default textures for ${mod.name} \`${modVersion.version}\`` + ); + return status; } diff --git a/src/server/actions/git.ts b/src/server/actions/git.ts new file mode 100644 index 00000000..f52dc4f3 --- /dev/null +++ b/src/server/actions/git.ts @@ -0,0 +1,122 @@ +'use server'; +import 'server-only'; + +import { Octokit } from '@octokit/rest'; +import { UserRole } from '@prisma/client'; + +import { auth } from '~/auth'; +import { canAccess } from '~/lib/auth'; +import { GITHUB_DEFAULT_REPO_NAME, GITHUB_ORG_NAME } from '~/lib/constants'; +import { db } from '~/lib/db'; + +import type { base64 } from '~/types'; + +export async function uploadToRepository(files: base64[], filenames: string[], commitMessage: string): Promise<void> { + await canAccess(UserRole.COUNCIL); + + const session = await auth(); + const user = session?.user!; // We know the user is logged in because of the canAccess check + + // Authenticate with GitHub API using current logged user's access token + const userToken = await db.account.findFirstOrThrow({ where: { userId: user.id }, select: { access_token: true } }); + const octokit = new Octokit({ + auth: userToken.access_token, + }); + + const branch = process.env.NODE_ENV === 'production' ? 'main' : 'dev'; + + // get latest commit + const currentCommit = await getCurrentCommit(octokit, branch); + + // create blobs for each file + const filesBlobs = await Promise.all(files.map((file) => createBlobFile(octokit, file))); + + // create new tree with the blobs + const newTree = await createNewTree(octokit, filenames, filesBlobs, currentCommit.tree_sha); + + // create a new commit with the new tree + const newCommit = await createCommit(octokit, commitMessage, newTree.sha, currentCommit.commit_sha); + + // update the branch to point to the new commit + await setBranchToCommit(octokit, newCommit.sha, branch); +} + +async function setBranchToCommit(octokit: Octokit, commitSha: string, branch: string) { + await octokit.git.updateRef({ + owner: GITHUB_ORG_NAME, + repo: GITHUB_DEFAULT_REPO_NAME, + ref: `heads/${branch}`, + sha: commitSha, + }); +} + +async function createCommit(octokit: Octokit, message: string, treeSha: string, parentCommitSha: string) { + const { data } = await octokit.git.createCommit({ + owner: GITHUB_ORG_NAME, + repo: GITHUB_DEFAULT_REPO_NAME, + message, + tree: treeSha, + parents: [parentCommitSha], + }); + + return data; +} + +/** + * Create a new tree with the given blobs + */ +async function createNewTree(octokit: Octokit, filenames: string[], blobs: { url: string, sha: string }[], parentTreeSha: string) { + const tree = blobs.map(({ sha }, index) => { + return { + path: filenames[index], + mode: '100644', + type: 'blob', + sha, + } as const; + }); + + const { data } = await octokit.git.createTree({ + owner: GITHUB_ORG_NAME, + repo: GITHUB_DEFAULT_REPO_NAME, + base_tree: parentTreeSha, + tree, + }); + + return data; +} + +/** + * Prepare a github blob for a file + */ +async function createBlobFile(octokit: Octokit, file: base64) { + const blobData = await octokit.git.createBlob({ + owner: GITHUB_ORG_NAME, + repo: GITHUB_DEFAULT_REPO_NAME, + content: file, + encoding: 'base64', + }); + + return blobData.data; +} + +/** + * Fetch the current commit of the given repository and branch + */ +async function getCurrentCommit(octokit: Octokit, branch: string) { + const { data: refData } = await octokit.git.getRef({ + owner: GITHUB_ORG_NAME, + repo: GITHUB_DEFAULT_REPO_NAME, + ref: `heads/${branch}`, + }); + + const { data: commitData } = await octokit.git.getCommit({ + owner: GITHUB_ORG_NAME, + repo: GITHUB_DEFAULT_REPO_NAME, + commit_sha: refData.object.sha, + }); + + return { + commit_sha: commitData.sha, + tree_sha: commitData.tree.sha, + }; +} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 6cec9b9d..3582832b 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -355,3 +355,5 @@ export type FPUser = { username?: string; uuid?: string; } + +export type base64 = `base64:${string}`; From 962504fee5a5800000dbe52a4231dd6410583674 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Sat, 17 Aug 2024 02:59:55 +0200 Subject: [PATCH 04/30] feat : archive contributions who's texture has been deleted --- src/server/data/resource.ts | 4 +++- src/server/data/texture.ts | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/server/data/resource.ts b/src/server/data/resource.ts index 0e4bbc64..ea41984b 100644 --- a/src/server/data/resource.ts +++ b/src/server/data/resource.ts @@ -6,6 +6,7 @@ import { UserRole } from '@prisma/client'; import { canAccess } from '~/lib/auth'; import { db } from '~/lib/db'; +import { deleteLinkedTexture } from './linked-textures'; import { deleteTexture } from './texture'; import type { Resource } from '@prisma/client'; @@ -79,12 +80,13 @@ export async function deleteResource(id: string): Promise<Resource> { const linkedTextures = await db.linkedTexture.findMany({ where: { resourceId: id } }); for (const linkedTexture of linkedTextures) { - await db.linkedTexture.delete({ where: { id: linkedTexture.id } }); + await deleteLinkedTexture(linkedTexture.id); const texture = await db.texture.findUnique({ where: { id: linkedTexture.textureId }, include: { linkedTextures: true }, }); + if (texture && texture.linkedTextures.length === 0) await deleteTexture(texture.id); } diff --git a/src/server/data/texture.ts b/src/server/data/texture.ts index c686a8ad..2b2dac01 100644 --- a/src/server/data/texture.ts +++ b/src/server/data/texture.ts @@ -1,7 +1,7 @@ 'use server'; import 'server-only'; -import { Resolution, UserRole } from '@prisma/client'; +import { Resolution, Status, UserRole } from '@prisma/client'; import { canAccess } from '~/lib/auth'; import { db } from '~/lib/db'; @@ -220,11 +220,16 @@ export async function deleteTexture(id: number): Promise<Texture> { await canAccess(UserRole.COUNCIL); // Delete on disk + // @deprecated (moved to git repository) const textureFile = await db.texture.findUnique({ where: { id } }).then((texture) => texture?.filepath); if (textureFile) await remove(textureFile as `/files/${string}`); // Contributions await db.contributionDeactivation.deleteMany({ where: { textureId: id } }); + await db.contribution.updateMany({ + where: { textureId: id }, + data: { textureId: null, status: Status.ARCHIVED }, + }); // Delete in database return db.texture.delete({ where: { id } }); From 640c882773b5e72c6e883a7666b694db3530cb3e Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Sun, 18 Aug 2024 00:49:57 +0200 Subject: [PATCH 05/30] feat : (part 1) new contribution system using git --- .../(protected)/contribute/about/page.tsx | 52 +- .../(pages)/(protected)/contribute/layout.tsx | 62 +-- .../(pages)/(protected)/contribute/page.tsx | 2 +- .../(protected)/contribute/settings/page.tsx | 127 +++++ .../contribute/submissions/page.tsx | 312 ++++++++++++ .../submit.scss => submissions/styles.scss} | 0 .../contribute/submit/co-authors-select.tsx | 63 --- .../contribute/submit/contribution-item.tsx | 124 ----- .../contribute/submit/contribution-modal.tsx | 455 ------------------ .../contribute/submit/contribution-tools.tsx | 172 ------- .../contribute/submit/delete-modal.tsx | 66 --- .../(protected)/contribute/submit/page.tsx | 278 ----------- .../(protected)/council/submissions/page.tsx | 11 +- src/app/(pages)/mods/[modId]/gallery/page.tsx | 4 +- src/auth.config.ts | 2 +- src/components/small-tile.tsx | 2 +- src/components/texture-contribution.tsx | 10 +- src/lib/constants.ts | 37 +- src/server/actions/files.ts | 4 +- src/server/actions/git.ts | 293 ++++++++++- src/server/data/contributions.ts | 266 ++++------ src/types/index.d.ts | 13 +- 22 files changed, 868 insertions(+), 1487 deletions(-) create mode 100644 src/app/(pages)/(protected)/contribute/settings/page.tsx create mode 100644 src/app/(pages)/(protected)/contribute/submissions/page.tsx rename src/app/(pages)/(protected)/contribute/{submit/submit.scss => submissions/styles.scss} (100%) delete mode 100644 src/app/(pages)/(protected)/contribute/submit/co-authors-select.tsx delete mode 100644 src/app/(pages)/(protected)/contribute/submit/contribution-item.tsx delete mode 100644 src/app/(pages)/(protected)/contribute/submit/contribution-modal.tsx delete mode 100644 src/app/(pages)/(protected)/contribute/submit/contribution-tools.tsx delete mode 100644 src/app/(pages)/(protected)/contribute/submit/delete-modal.tsx delete mode 100644 src/app/(pages)/(protected)/contribute/submit/page.tsx diff --git a/src/app/(pages)/(protected)/contribute/about/page.tsx b/src/app/(pages)/(protected)/contribute/about/page.tsx index c336556e..089a1d45 100644 --- a/src/app/(pages)/(protected)/contribute/about/page.tsx +++ b/src/app/(pages)/(protected)/contribute/about/page.tsx @@ -1,52 +1,6 @@ -'use client'; - -import { Button, Badge, Text, Checkbox } from '@mantine/core'; -import { useLocalStorage } from '@mantine/hooks'; - -import { Tile } from '~/components/tile'; -import { useDeviceSize } from '~/hooks/use-device-size'; -import { BREAKPOINT_MOBILE_LARGE, COLORS } from '~/lib/constants'; - -const ContributeAboutPage = () => { - const [windowWidth] = useDeviceSize(); - - const [showAboutPage, setShowAboutPage] = useLocalStorage({ - key: 'faithful-modded-show-contribute-about-page', - defaultValue: true, - }); +export default function ContributeAboutPage() { return ( - <> - {/* <Text size="md" fw={700} mb="sm">Submission Process</Text> */} - <Tile> - {windowWidth > BREAKPOINT_MOBILE_LARGE && <Button pos="absolute" right="var(--mantine-spacing-md)" disabled>Apply for Council</Button>} - <Text size="sm"> - Once submitted, your submissions are subject to a voting process by the council and their decision is final.<br /> - When all counselors have voted, the following will happen: - </Text> - <ul> - <Text size="sm" component="li"> - If the contribution has more upvotes than downvotes, it will be <Badge component="span" color="teal">accepted</Badge> - </Text> - <Text size="sm" component="li"> - If there is more downvotes or the same amount of upvotes and downvotes, it will be <Badge component="span" color={COLORS.REJECTED}>rejected</Badge> and deleted after a 6-month period. - </Text> - </ul> - <Text size="sm"> - You can edit your submissions as many times as you like. <br/> - Note that if you edit your contribution (even when rejected), its status will be reset to <Badge component="span" color={COLORS.DRAFT}>draft</Badge> and will need to be re-submitted and re-voted on. - </Text> - {windowWidth <= BREAKPOINT_MOBILE_LARGE && <Button mt="sm" disabled>Apply for Council</Button>} - - <Checkbox - mt="md" - checked={!showAboutPage} - onChange={(e) => setShowAboutPage(!e.target.checked)} - label={<Text size="sm" c="dimmed">I have read and understood the submission process, don't show this page again</Text>} - /> - </Tile> - </> + 'WIP' ); -}; - -export default ContributeAboutPage; +} diff --git a/src/app/(pages)/(protected)/contribute/layout.tsx b/src/app/(pages)/(protected)/contribute/layout.tsx index 8c287aff..d3712e59 100644 --- a/src/app/(pages)/(protected)/contribute/layout.tsx +++ b/src/app/(pages)/(protected)/contribute/layout.tsx @@ -1,58 +1,22 @@ 'use client'; -import { GoAlert } from 'react-icons/go'; - -import { CloseButton, Group, Text } from '@mantine/core'; -import { useLocalStorage } from '@mantine/hooks'; - import { TabsLayout } from '~/components/tabs'; -import { Tile } from '~/components/tile'; -interface ProtectedLayoutProps { +interface Props { children: React.ReactNode; -}; - -const ContributeLayout = ({ children }: ProtectedLayoutProps) => { - const [isTOSShown, setShown] = useLocalStorage({ - key: 'faithful-mods-contribute-tos', - defaultValue: true, - }); - - const tabs = [ - { value: 'about', label: 'About' }, - { value: 'submit', label: 'Submit' }, - ]; +} +export default function ContributeLayout({ children }: Props) { return ( - <> - {isTOSShown && ( - <Tile color="yellow" mb="md" p="xs" pl="md"> - <Group justify="space-between"> - <Group gap="xs" wrap="nowrap"> - <GoAlert color="black" /> - <Text size="sm" c="black" > - By contributing to the platform, you agree to the <Text component="a" href="/docs/tos" c="brown" target="_blank">Terms of Service</Text>.<br /> - </Text> - </Group> - <CloseButton - variant="transparent" - style={{ color: 'black' }} - onClick={() => setShown(false)} - /> - </Group> - </Tile> - )} - - <TabsLayout - tabs={tabs} - defaultValue="about" - variant="filled" - noMargin - > - {children} - </TabsLayout> - </> + <TabsLayout + tabs={[ + { value: 'about', label: 'About' }, + { value: 'submissions', label: 'Submissions' }, + { value: 'settings', label: 'Settings' }, + ]} + > + {children} + </TabsLayout> ); -}; -export default ContributeLayout; +} diff --git a/src/app/(pages)/(protected)/contribute/page.tsx b/src/app/(pages)/(protected)/contribute/page.tsx index 8aac30c4..8194e6a9 100644 --- a/src/app/(pages)/(protected)/contribute/page.tsx +++ b/src/app/(pages)/(protected)/contribute/page.tsx @@ -12,7 +12,7 @@ const CouncilPage = () => { }); if (isAboutShown) redirect('/contribute/about'); - redirect('/contribute/submit'); + redirect('/contribute/submissions'); }; export default CouncilPage; diff --git a/src/app/(pages)/(protected)/contribute/settings/page.tsx b/src/app/(pages)/(protected)/contribute/settings/page.tsx new file mode 100644 index 00000000..71abb7dc --- /dev/null +++ b/src/app/(pages)/(protected)/contribute/settings/page.tsx @@ -0,0 +1,127 @@ +'use client'; + +import Link from 'next/link'; + +import { useState, useTransition } from 'react'; + +import { GoCheckCircle, GoStop } from 'react-icons/go'; + +import { Button, Group, Stack, Text } from '@mantine/core'; + +import { Tile } from '~/components/tile'; +import { useEffectOnce } from '~/hooks/use-effect-once'; +import { getFork, deleteFork, forkRepository } from '~/server/actions/git'; + +export default function ContributeSettingsPage() { + const [forked, setHasFork] = useState<string | null>(null); + const [loading, startTransition] = useTransition(); + + useEffectOnce(() => { + reload(); + }); + + const reload = async () => { + startTransition(async () => { + setHasFork(await getFork()); + }); + }; + + const handleSetupForkedRepository = async () => { + startTransition(async () => { + await forkRepository(); + await reload(); + }); + }; + + const handleForkDelete = async () => { + startTransition(async () => { + await deleteFork(); + await reload(); + }); + }; + + const forkedInfo = () => { + if (forked) { + return ( + <Tile p="xs" pl="md" color="teal"> + <Group justify="space-between"> + <Group> + <GoCheckCircle size={20} color="white"/> + <Group gap={3}> + <Text size="sm" c="white">Default textures repository forked: </Text> + <Text size="sm" c="white"><Link href={forked} style={{ color: 'white' }}>{forked}</Link></Text> + </Group> + </Group> + + <Group gap="xs"> + <Button variant="outline" color="white">Sync Fork</Button> + </Group> + </Group> + </Tile> + ); + } + + return ( + <Tile p="xs" pl="md" color="yellow"> + <Group justify="space-between"> + <Group> + <GoStop color="black" size={20} /> + <Group gap="xs"> + <Text size="sm" c="black">Default textures repository not forked</Text> + </Group> + </Group> + + <Group gap="xs"> + <Button + variant="outline" + color="black" + onClick={handleSetupForkedRepository} + disabled={!!forked} + loading={loading} + > + Create Fork + </Button> + </Group> + </Group> + </Tile> + ); + }; + + return ( + <Stack gap="xl"> + <Stack gap="xs"> + <Text fw={700}>General</Text> + {forkedInfo()} + </Stack> + + <Stack gap="xs"> + <Text fw={700}>Danger Zone</Text> + <Tile + p="xs" + pl="md" + withBorder + style={{ + backgroundColor: 'transparent', + borderColor: 'var(--mantine-color-red-filled)', + }} + > + <Group justify="space-between"> + <Stack gap={0}> + <Text>Delete the forked repository</Text> + <Text c="dimmed" size="xs">This action is irreversible, all contributions will be lost.</Text> + </Stack> + <Button + variant="default" + style={{ color: 'var(--mantine-color-red-text)' }} + onClick={handleForkDelete} + disabled={!forked} + loading={loading} + > + Delete Fork + </Button> + </Group> + </Tile> + </Stack> + </Stack> + ); +} diff --git a/src/app/(pages)/(protected)/contribute/submissions/page.tsx b/src/app/(pages)/(protected)/contribute/submissions/page.tsx new file mode 100644 index 00000000..cc6f1edc --- /dev/null +++ b/src/app/(pages)/(protected)/contribute/submissions/page.tsx @@ -0,0 +1,312 @@ +'use client'; + +import { useCallback, useEffect, useState, useTransition } from 'react'; + +import { GoCommit, GoHash, GoHourglass, GoRelFilePath } from 'react-icons/go'; +import { IoReload } from 'react-icons/io5'; +import { LuArrowUpDown } from 'react-icons/lu'; + +import { ActionIcon, Button, FloatingIndicator, Group, Indicator, Select, Stack, Text } from '@mantine/core'; +import { usePrevious } from '@mantine/hooks'; +import { Resolution, Status } from '@prisma/client'; + +import { SmallTile } from '~/components/small-tile'; +import { TextureImage } from '~/components/texture-img'; +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, COLORS, gitBlobUrl, gitCommitUrl } from '~/lib/constants'; +import { getContributionsOfFork, getFork } from '~/server/actions/git'; +import { createContributionsFromGitFiles, deleteContributionsOrArchive, getContributionsOfUser } from '~/server/data/contributions'; +import { getTextures } from '~/server/data/texture'; + +import type { Texture } from '@prisma/client'; +import type { GitFile } from '~/server/actions/git'; +import type { GetContributionsOfUser } from '~/server/data/contributions'; + +import './styles.scss'; + +export default function ContributeSubmissionsPage() { + const user = useCurrentUser()!; // the user is guaranteed to be logged in (per the layout) + + const [loading, startTransition] = useTransition(); + const [hasFork, setHasFork] = useState<string | null>(null); + + const [resolution, setResolution] = useState<Resolution>(Resolution.x32); + const prevRes = usePrevious(resolution); + + const [contributions, setContributions] = useState<GetContributionsOfUser[]>([]); + const [textures, setTextures] = useState<Texture[]>([]); + + const [groupRef, setGroupRef] = useState<HTMLDivElement | null>(null); + const [controlsRefs, setControlsRefs] = useState<Record<string, HTMLButtonElement | null>>({}); + const [activeTab, setActiveTab] = useState(0); + const [windowWidth] = useDeviceSize(); + + const setControlRef = (index: number) => (node: HTMLButtonElement) => { + controlsRefs[index] = node; + setControlsRefs(controlsRefs); + }; + + const reload = async () => { + startTransition(async () => { + const fork = await getFork(); + if (!fork) return; + + setHasFork(fork); + getContributionsOfFork(resolution).then(updateForkContributions); + }); + }; + + const updateForkContributions = useCallback(async (files: GitFile[]) => { + const contributions = await getContributionsOfUser(user.id!, resolution); + const contributedSha = contributions.map((contribution) => contribution.hash); + + // delete contributions that are not in the fork but are in the database + const missingFiles = contributions.filter((contribution) => !files.some((file) => file.sha === contribution.hash)); + await deleteContributionsOrArchive(user.id!, missingFiles.map((contribution) => contribution.id)); + + // add contributions that are not yet in the database + const newFiles = files.filter((file) => !contributedSha.includes(file.sha)); + await createContributionsFromGitFiles(user.id!, resolution, newFiles); + + const contributionsAfter = await getContributionsOfUser(user.id!, resolution); + setContributions(contributionsAfter); + + }, [user, resolution]); + + useEffectOnce(() => { + reload(); + getTextures().then(setTextures); + }); + + useEffect(() => { + if (prevRes === resolution) return; + + startTransition(() => { + getContributionsOfFork(resolution).then(updateForkContributions); + }); + }, [resolution, prevRes, updateForkContributions]); + + const controls = Object.entries(Status).map(([key, value], index) => { + const isLast = index === Object.keys(Status).length - 1; + + return ( + <Button + key={key} + ref={setControlRef(index)} + onClick={() => setActiveTab(index)} + mod={{ active: activeTab === index }} + + pl="md" + pr="sm" + fullWidth + variant="filled" + leftSection={ + <> + <Indicator color={COLORS[value]} mr="md" /> + {value === Status.DRAFT + ? 'Drafted' + : value === Status.PENDING + ? 'Reviewed' + : value.charAt(0).toUpperCase() + value.slice(1).toLowerCase() + } + </> + } + rightSection={0} + justify="space-between" + className="slider-button" + style={windowWidth > BREAKPOINT_MOBILE_LARGE + ? { + borderRight: !isLast && activeTab !== index + 1 && activeTab !== index + ? 'calc(0.0625rem * var(--mantine-scale)) solid var(--mantine-color-default-border)' + : undefined, + borderRadius: isLast ? '0 calc(0.25rem * var(--mantine-scale)) calc(0.25rem * var(--mantine-scale)) 0' : undefined, + } + : undefined + } + /> + ); + }); + + return ( + <Stack gap="xs"> + <Group + wrap="nowrap" + gap="xs" + > + <ActionIcon + variant="default" + className="navbar-icon-fix" + onClick={reload} + loading={loading} + > + <IoReload /> + </ActionIcon> + <Select + w={120} + data={Object.keys(Resolution)} + checkIconPosition="right" + value={resolution} + onChange={(e) => e ? setResolution(e as Resolution) : null} + clearable={false} + /> + <Button.Group + w="calc(100% - 120px - var(--mantine-spacing-xs))" + ref={setGroupRef} + style={{ + position: 'relative', + }} + orientation={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'vertical' : 'horizontal'} + > + {controls} + + <FloatingIndicator + target={controlsRefs[activeTab]} + parent={groupRef} + style={{ + border: 'calc(0.0625rem * var(--mantine-scale)) solid #fff3', + borderRadius: (() => { + if (activeTab === 0) return 'calc(0.25rem * var(--mantine-scale)) 0 0 calc(0.25rem * var(--mantine-scale))'; + if (activeTab === Object.keys(Status).length - 1) return '0 calc(0.25rem * var(--mantine-scale)) calc(0.25rem * var(--mantine-scale)) 0'; + return '0'; + })(), + backgroundColor: '#0002', + cursor: 'pointer', + zIndex: 200, + }} + /> + + </Button.Group> + </Group> + + {hasFork === null && ( + <Group + align="center" + justify="center" + h="100px" + w="100%" + style={{ height: 'calc(81% - (2 * var(--mantine-spacing-sm) - 62px))' }} + > + <Text c="dimmed">You need to fork the default repository first</Text> + </Group> + )} + + {hasFork && ( + <Group gap="xs"> + {contributions.map((contribution) => { + const texture = textures.find((t) => t.id === contribution.textureId); + const orgOrUser = contribution.filepath.split('/')[3]!; + const repository = contribution.filepath.split('/')[4]!; + const commitSha = contribution.filepath.split('/')[5]!; + + return ( + ( + <TextureImage + key={contribution.id} + src={contribution.filepath} + alt={contribution.filename} + popupStyles={{ + backgroundColor: 'transparent', + padding: 0, + border: 'none', + boxShadow: 'none', + }} + > + <Group gap={2}> + {texture && ( + <SmallTile color="gray" w={125} h="100%"> + <Group w="100%" h="100%" justify="center" align="center"> + <TextureImage + src={texture.filepath} + alt={texture.name} + mcmeta={texture.mcmeta} + size={115} + /> + </Group> + </SmallTile> + )} + <Stack gap={2} align="start" miw={400} maw={400}> + <SmallTile color="gray"> + <Text fw={500} ta="center">{texture?.name}</Text> + </SmallTile> + + <Group gap={2} w="100%" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <GoCommit /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs"> + <a + href={gitCommitUrl({ orgOrUser, repository, commitSha })} + target="_blank" + rel="noreferrer" + > + {commitSha} + </a> + </Text> + </SmallTile> + </Group> + + <Group gap={2} w="100%" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <GoRelFilePath /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs"> + <a + href={gitBlobUrl({ orgOrUser, repository, branchOrCommit: commitSha, path: contribution.filename })} + target="_blank" + rel="noreferrer" + > + {contribution.filename} + </a> + </Text> + </SmallTile> + </Group> + + <Group gap={2} w="100%"> + <Group gap={2} w="calc((100% - 4px) / 3)" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <LuArrowUpDown /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs"> + {contribution.poll.upvotes.length - contribution.poll.downvotes.length} + </Text> + </SmallTile> + </Group> + <Group gap={2} w="calc((100% - 4px) / 3)" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <GoHash /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs"> + {contribution.textureId} + </Text> + </SmallTile> + </Group> + <Group gap={2} w="calc((100% - 4px) / 3)" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <GoHourglass /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs"> + {contribution.status} + </Text> + </SmallTile> + </Group> + </Group> + </Stack> + </Group> + </TextureImage> + ) + ); + })} + </Group> + )} + + </Stack> + ); +} + diff --git a/src/app/(pages)/(protected)/contribute/submit/submit.scss b/src/app/(pages)/(protected)/contribute/submissions/styles.scss similarity index 100% rename from src/app/(pages)/(protected)/contribute/submit/submit.scss rename to src/app/(pages)/(protected)/contribute/submissions/styles.scss diff --git a/src/app/(pages)/(protected)/contribute/submit/co-authors-select.tsx b/src/app/(pages)/(protected)/contribute/submit/co-authors-select.tsx deleted file mode 100644 index b2c88add..00000000 --- a/src/app/(pages)/(protected)/contribute/submit/co-authors-select.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useState } from 'react'; - -import { Avatar, Group, MultiSelect, Text } from '@mantine/core'; - -import { useEffectOnce } from '~/hooks/use-effect-once'; -import { notify } from '~/lib/utils'; -import { getPublicUsers } from '~/server/data/user'; - -import type { MultiSelectProps } from '@mantine/core'; -import type { useCurrentUser } from '~/hooks/use-current-user'; -import type { 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<ReturnType<typeof useCurrentUser>>; - onCoAuthorsSelect: (coAuthors: PublicUser[]) => void; -} - -export function CoAuthorsSelector({ author, onCoAuthorsSelect, ...props }: CoAuthorsSelectorProps) { - const [users, setUsers] = useState<PublicUser[]>([]); - - 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 ( - <Group gap="sm" wrap="nowrap"> - <Avatar src={user.image} size={30} radius="xl" /> - <div> - <Text size="sm">{option.label}</Text> - {option.disabled && <Text size="xs" c="dimmed">That's you!</Text>} - </div> - </Group> - ); - }; - - return ( - <MultiSelect - limit={10} - label="Co-authors" - placeholder="Select or search co-authors..." - data={users.map((u) => ({ 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} - /> - ); -} diff --git a/src/app/(pages)/(protected)/contribute/submit/contribution-item.tsx b/src/app/(pages)/(protected)/contribute/submit/contribution-item.tsx deleted file mode 100644 index 16592665..00000000 --- a/src/app/(pages)/(protected)/contribute/submit/contribution-item.tsx +++ /dev/null @@ -1,124 +0,0 @@ - -import { useState } from 'react'; - -import { LuArrowUpDown } from 'react-icons/lu'; -import { RxCross2 } from 'react-icons/rx'; - -import { Button, Code, Group, Stack, Text } from '@mantine/core'; -import { Status } from '@prisma/client'; - -import { TextureImage } from '~/components/texture-img'; -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 { removeCoAuthor } from '~/server/data/contributions'; -import { getPollResult } from '~/server/data/polls'; - -import type { CSSProperties } from '@mantine/core'; -import type { ContributionWithCoAuthorsAndPoll, PollResults } from '~/types'; - -export interface ContributionPanelItemProps { - contribution: ContributionWithCoAuthorsAndPoll; - isCoAuthorContribution?: boolean; - onClick?: (c: ContributionWithCoAuthorsAndPoll) => void; - onUpdate?: () => void; - styles?: CSSProperties; -} - -export function ContributionPanelItem({ contribution, onClick, onUpdate, styles, isCoAuthorContribution }: ContributionPanelItemProps) { - const [windowWidth] = useDeviceSize(); - const imgWidth = windowWidth <= BREAKPOINT_MOBILE_LARGE ? 60 : 90; - const user = useCurrentUser()!; // the user is guaranteed to be logged in (per the layout) - - const [poll, setPoll] = useState<PollResults | null>(null); - const isDraft = contribution.status === Status.DRAFT; - - useEffectOnce(() => { - getPollResult(contribution.pollId).then(setPoll); - }); - - return ( - <Stack gap="sm" align="center"> - - <TextureImage - src={contribution.filepath} - alt="" - size={imgWidth} - mcmeta={contribution.mcmeta} - onClick={() => onClick?.(contribution)} - styles={styles} - > - <Group align="baseline" justify="space-between" > - <Group> - <Text size="sm" mb="sm" fw={700}>{contribution.filename}</Text> - </Group> - {!isDraft && poll && ( - <Group gap="xs" justify="center"> - <Text component="span" size="xs">{poll.upvotes - poll.downvotes}</Text> - <Text size="xs" style={{ display: 'flex' }}><LuArrowUpDown /></Text> - </Group> - )} - </Group> - <Stack gap={2} align="flex-start" mt="0"> - {isCoAuthorContribution && ( - <Group> - <Text w={70} size="xs">Author: </Text> - <Text size="xs"><Code component="span">{contribution.owner.name}</Code></Text> - </Group> - )} - {contribution.coAuthors.length > 0 && (<Group> - <Text w={70} size="xs">Co-authors:</Text> - <Text size="xs">{contribution.coAuthors.map((ca) => { - if (ca.id === user.id) { - return( - <Button - key={ca.id} - component="span" - color="red" - leftSection={<RxCross2 />} - h={18} - mr={2} - mt={-1} - style={{ - fontSize: 'var(--mantine-font-size-xs)', - fontFamily: 'var(--mantine-font-family-monospace)', - lineHeight: 'var(--mantine-line-height)', - padding: '2px calc(var(--mantine-spacing-xs) / 2)', - }} - onClick={() => { - removeCoAuthor(user.id!, ca.id, contribution.id); - onUpdate?.(); - }} - > - {ca.name} - </Button> - ); - } - return <Code component="span" key={ca.id} mr={2}>{ca.name}</Code>; - })}</Text> - </Group>)} - <Group> - <Text w={70} size="xs">Texture ID:</Text> - <Text size="xs"> - {contribution.textureId && <Code component="span">{contribution.textureId}</Code>} - {!contribution.textureId && '-'} - </Text> - </Group> - <Group> - <Text w={70} size="xs">Resolution:</Text> - <Text size="xs"><Code component="span">{contribution.resolution}</Code></Text> - </Group> - <Group> - <Text w={70} size="xs">Drafted:</Text> - <Text size="xs"><Code component="span">{contribution.createdAt.toLocaleString()}</Code></Text> - </Group> - <Group> - <Text w={70} size="xs">Updated:</Text> - <Text size="xs"><Code component="span">{contribution.updatedAt.toLocaleString()}</Code></Text> - </Group> - </Stack> - </TextureImage> - </Stack> - ); -} diff --git a/src/app/(pages)/(protected)/contribute/submit/contribution-modal.tsx b/src/app/(pages)/(protected)/contribute/submit/contribution-modal.tsx deleted file mode 100644 index ec864d54..00000000 --- a/src/app/(pages)/(protected)/contribute/submit/contribution-modal.tsx +++ /dev/null @@ -1,455 +0,0 @@ -import { useMemo, useRef, useState, useTransition } from 'react'; - -import { FaChevronLeft, FaChevronRight } from 'react-icons/fa'; -import { PiMagicWandBold, PiFileArrowUpBold } from 'react-icons/pi'; - -import { Button, Code, Container, Divider, FileButton, Group, JsonInput, Select, Stack, Text, TextInput, Title } from '@mantine/core'; -import { Resolution } from '@prisma/client'; - -import { TextureImage } from '~/components/texture-img'; -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, GRADIENT, GRADIENT_DANGER } from '~/lib/constants'; -import { getContributionsOfTexture, updateContributionPicture, updateDraftContribution } from '~/server/data/contributions'; - -import { CoAuthorsSelector } from './co-authors-select'; - -import type { MultiSelectProps } from '@mantine/core'; -import type { Texture } from '@prisma/client'; -import type { GetTexturesWithUsePaths } from '~/server/data/texture'; -import type { ContributionWithCoAuthors, ContributionWithCoAuthorsAndPoll, PublicUser } from '~/types'; - -export interface ContributionModalProps { - contribution: ContributionWithCoAuthors; - textures: GetTexturesWithUsePaths[]; - onClose: (editedContribution: ContributionWithCoAuthors) => void; -} - -export function ContributionModal({ contribution, textures, onClose }: ContributionModalProps) { - const [isPending, startTransition] = useTransition(); - const [selectedTexture, setSelectedTexture] = useState<Texture | null>(null); - const [selectedCoAuthors, setSelectedCoAuthors] = useState<PublicUser[]>(contribution.coAuthors); - const [selectedResolution, setSelectedResolution] = useState<Resolution>(contribution.resolution); - const [windowWidth] = useDeviceSize(); - - const stackRef = useRef<HTMLDivElement>(null); - - const colWidth = useMemo(() => { - const width = stackRef.current?.parentElement?.clientWidth ?? 0; - return windowWidth <= BREAKPOINT_MOBILE_LARGE ? `${width * .93}px` : `calc((${width}px - (2 * var(--mantine-spacing-md))) / 3.25)`; - }, - [stackRef, windowWidth]); - - const columnStyle = { - width: colWidth, - }; - - const [selectedTextureId, setSelectedTextureId] = useState<number | null>(null); - const [selectedTextureContributions, setSelectedTextureContributions] = useState<ContributionWithCoAuthorsAndPoll[]>([]); - const [selectedTextureContributionsIndex, setSelectedTextureContributionsIndex] = useState<number>(0); - const [displayedSelectedTextureContributions, setDisplayedSelectedTextureContributions] = useState<ContributionWithCoAuthorsAndPoll | undefined>(); - - const [disabledResolution, setDisabledResolution] = useState<(Resolution | null)[]>([]); - - const [contributionMCMETA, setContributionMCMETA] = useState<string>(contribution.mcmeta ? JSON.stringify(contribution.mcmeta, null, 2) : ''); - const parsedMCMETA = useMemo(() => { - try { - return JSON.parse(contributionMCMETA); - } catch { - return null; - } - }, [contributionMCMETA]); - - const author = useCurrentUser()!; - - useEffectOnce(() => { - if (contribution.textureId) handleTextureSelected(contribution.textureId); - }); - - const handleTextureSelected = (textureId: number | null) => { - if (textureId === null) { - setSelectedTexture(null); - setSelectedTextureId(null); - setSelectedTextureContributions([]); - setSelectedTextureContributionsIndex(0); - setDisplayedSelectedTextureContributions(undefined); - setContributionMCMETA(''); - setDisabledResolution([]); - return; - } - - const texture = textures.find((t) => t.id === textureId)!; - const disabled = texture.disabledContributions.map((d) => d.resolution); - setDisabledResolution(disabled); - setSelectedTexture(texture); - setSelectedTextureId(texture.id); - - if (!contributionMCMETA && texture.mcmeta) setContributionMCMETA(JSON.stringify(texture.mcmeta, null, 2)); - - getContributionsOfTexture(textureId) - .then((res) => { - // remove the current contribution from the list - res = res.filter((c) => c.resolution === selectedResolution && c.id !== contribution.id); - - setSelectedTextureContributions(res); - setSelectedTextureContributionsIndex(0); - setDisplayedSelectedTextureContributions(res[0]); - }) - .catch(console.error); - }; - - const renderMultiSelectOption: MultiSelectProps['renderOption'] = ({ option }) => { - const texture = textures.find((u) => u.id === parseInt(option.value, 10))!; - - return ( - <Stack gap="sm" className="w-full"> - <Group gap="sm" wrap="nowrap" align="start"> - <TextureImage src={texture.filepath} alt="" size={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 60 : 160} mcmeta={texture.mcmeta} /> - <Stack gap={2} className="w-full"> - <Group wrap="nowrap" className="w-full"> - {windowWidth > BREAKPOINT_MOBILE_LARGE && ( - <Text w={50} size="sm" fw={400}>Name:</Text> - )} - <Code>{texture.name}</Code> - </Group> - {texture.aliases.length > 0 && ( - <Group wrap="nowrap" className="w-full"> - {windowWidth > BREAKPOINT_MOBILE_LARGE && ( - <Text w={50} size="sm" fw={400}>Aliases:</Text> - )} - <Group gap={2}> - {texture.aliases.map((a, index) => <Code key={index}>{a}</Code>)} - </Group> - </Group> - )} - {windowWidth > BREAKPOINT_MOBILE_LARGE && ( - <Group align="start" wrap="nowrap" className="w-full"> - <Text w={50} size="sm" fw={400}>Uses:</Text> - <Stack gap={2}> - {texture.linkedTextures.map((t, index) => - <Code key={index}>{t.assetPath}</Code> - )} - </Stack> - </Group> - )} - </Stack> - </Group> - - {windowWidth <= BREAKPOINT_MOBILE_LARGE && ( - <Stack gap={2}> - {texture.linkedTextures.map((t, index) => - <Code key={index}>{t.assetPath}</Code> - )} - </Stack> - )} - </Stack> - ); - }; - - const handlePrevContribution = () => { - if (selectedTextureContributionsIndex === 0) return; - let index = selectedTextureContributionsIndex - 1; - setSelectedTextureContributionsIndex(index); - setDisplayedSelectedTextureContributions(selectedTextureContributions[index]); - }; - - const handleNextContribution = () => { - if (selectedTextureContributionsIndex === selectedTextureContributions.length - 1) return; - let index = selectedTextureContributionsIndex + 1; - setSelectedTextureContributionsIndex(index); - setDisplayedSelectedTextureContributions(selectedTextureContributions[index]); - }; - - const handleContributionFileChange = (file: File | null) => { - if (!file) return; - - const formData = new FormData(); - formData.append('file', file); - - startTransition(() => { - updateContributionPicture(author.id!, contribution.id, formData) - .then(() => onClose(contribution)); - }); - }; - - const handleDraftUpdate = () => { - if (!selectedTexture) return; - - startTransition(() => { - updateDraftContribution({ - ownerId: author.id!, - contributionId: contribution.id, - coAuthors: selectedCoAuthors.map((c) => c.id), - resolution: selectedResolution, - textureId: selectedTexture.id, - mcmeta: parsedMCMETA, - }) - .then(onClose) - .catch(console.error); - }); - }; - - const handleCancel = () => { - onClose(contribution); - }; - - return ( - <Stack gap="lg" className="w-full" ref={stackRef} align="center"> - <Group gap="md" align="start"> - {/* User contribution */} - <Stack gap="md" align="left" justify="space-between" style={columnStyle}> - <Stack gap="0"> - <Title order={5}>Yours</Title> - <Text size="sm" c="dimmed">The file you're submitting.</Text> - </Stack> - <Group wrap="nowrap"> - <FileButton accept="image/png" onChange={handleContributionFileChange}> - {(props) => ( - <Button - variant="light" - color="blue" - p={0} - className="navbar-icon-fix" - loading={isPending} - {...props} - > - <PiFileArrowUpBold /> - </Button> - )} - </FileButton> - <TextInput - value={contribution.filename} - disabled - className="w-full" - /> - </Group> - <TextureImage - src={contribution.filepath} - mcmeta={parsedMCMETA} - size={colWidth} - alt="" - /> - <JsonInput - label={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'Custom MCMETA' : <></>} - validationError="Invalid JSON" - formatOnBlur - autosize - minRows={4} - - value={contributionMCMETA} - onChange={setContributionMCMETA} - /> - </Stack> - {/* Default texture */} - <Stack gap="md" align="left" justify="space-between" style={columnStyle}> - <Stack gap="0"> - <Title order={5}>Default Texture</Title> - <Text size="sm" c="dimmed">The targeted texture.</Text> - </Stack> - <Group wrap="nowrap"> - <Button - variant="light" - color="blue" - p={0} - className="navbar-icon-fix" - loading={isPending} - onClick={() => { - startTransition(() => { - const texture = textures.find((t) => t.name === contribution.filename.replace('.png', '') || t.aliases.includes(contribution.filename.replace('.png', ''))); - if (texture) handleTextureSelected(texture.id); - }); - }} - > - <PiMagicWandBold /> - </Button> - <Select - limit={100} - data={textures.map((t) => ({ value: t.id.toString(), label: `${t.name} ${t.aliases.join(' ')} ${t.linkedTextures.map((l) => l.assetPath).join(' ')} ${t.id}`, disabled: t.id === selectedTextureId }))} - defaultValue={contribution.textureId?.toString()} - value={selectedTextureId?.toString()} - renderOption={renderMultiSelectOption} - className="w-full" - onChange={(e) => handleTextureSelected(e ? parseInt(e, 10) : null)} - onClear={() => setSelectedTexture(null)} - maxDropdownHeight={colWidth} - comboboxProps={windowWidth > BREAKPOINT_MOBILE_LARGE - ? { - width: `calc((${colWidth} * 3) + 2 * var(--mantine-spacing-md))`, - offset: { - mainAxis: 5, - crossAxis: -27, - }, - } - : { - width: colWidth, - offset: { - mainAxis: 5, - crossAxis: -27, - }, - } - } - placeholder="Search a texture by its name/path..." - searchable - required - clearable - /> - </Group> - {selectedTexture && - <TextureImage - src={selectedTexture.filepath} - size={colWidth} - mcmeta={selectedTexture?.mcmeta} - alt="" - /> - } - {!selectedTexture && <Container className="texture-background" pt="100%" pl="calc(100% - var(--mantine-spacing-md))" />} - <JsonInput - label={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'Default MCMETA' : <Title order={6} ta="center">MCMETA</Title>} - labelProps={{ style: { width: '100%' } }} - validationError="Invalid JSON" - formatOnBlur - autosize - minRows={4} - - value={(selectedTexture?.mcmeta && JSON.stringify(selectedTexture?.mcmeta, null, 2)) ?? ''} - disabled - /> - </Stack> - {/* Existing contribution */} - <Stack gap="md" align="left" justify="space-between" style={columnStyle}> - <Stack gap="0"> - <Title order={5}>Existing Contributions</Title> - <Text size="sm" c="dimmed">Of the targeted texture.</Text> - </Stack> - <Group gap="md" justify="center"> - <Button - variant="light" - disabled={selectedTextureContributions.length === 0 || selectedTextureContributionsIndex === 0} - onClick={handlePrevContribution} - > - <FaChevronLeft/> - </Button> - {selectedTextureContributions.length > 0 && - <Group w={50} align="center"> - <Text w={50} ta="center">{selectedTextureContributionsIndex + 1} / {selectedTextureContributions.length}</Text> - </Group> - } - {selectedTextureContributions.length === 0 && - <Group w={50} align="center"> - <Text w={50} ta="center">- / -</Text> - </Group> - } - <Button - variant="light" - disabled={selectedTextureContributions.length === 0 || selectedTextureContributionsIndex === selectedTextureContributions.length - 1} - onClick={handleNextContribution} - > - <FaChevronRight/> - </Button> - </Group> - {!selectedTexture && selectedTextureContributions.length === 0 && - <Container className="texture-background" pt="100%" pl="calc(100% - var(--mantine-spacing-md))" pos="relative"> - <Text - size="sm" - pos="absolute" - left="0" - right="0" - top="calc(50% - (20.3px /2))" // text height / 2 - style={{ textAlign: 'center' }} - > - Select a texture first! - </Text> - </Container> - } - {selectedTexture && selectedTextureContributions.length === 0 && - <Container className="texture-background" pt="100%" pl="calc(100% - var(--mantine-spacing-md))" pos="relative"> - <Text - size="sm" - pos="absolute" - left="0" - right="0" - top="calc(50% - (20.3px /2))" // text height / 2 - style={{ textAlign: 'center' }} - > - No contributions for this texture. - </Text> - </Container> - } - {selectedTexture && selectedTextureContributions.length > 0 && displayedSelectedTextureContributions && - <TextureImage - src={displayedSelectedTextureContributions.filepath} - size={colWidth} - mcmeta={displayedSelectedTextureContributions?.mcmeta} - alt="" - /> - } - <JsonInput - label={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'Contribution MCMETA' : <></>} - validationError="Invalid JSON" - formatOnBlur - autosize - minRows={4} - - value={JSON.stringify(displayedSelectedTextureContributions?.mcmeta, null, 2)} - disabled - /> - </Stack> - - </Group> - - <Divider size="xs" w={windowWidth <= BREAKPOINT_MOBILE_LARGE ? '90%' : `calc(${colWidth} * 1.5)`}/> - - <Group gap="md" justify="center" > - <Select - label="Resolution" - data={(Object.keys(Resolution) as Resolution[]).filter((r) => !disabledResolution.includes(r))} - disabled={disabledResolution.length === Object.keys(Resolution).length || disabledResolution.includes(null)} - allowDeselect={false} - defaultValue={contribution.resolution} - onChange={(value) => setSelectedResolution(value as Resolution)} - style={windowWidth <= BREAKPOINT_MOBILE_LARGE ? { width: '100%' } : { width: 'calc((100% - var(--mantine-spacing-md)) * .2)' }} - required - /> - <CoAuthorsSelector - author={author} - onCoAuthorsSelect={setSelectedCoAuthors} - defaultValue={selectedCoAuthors.map((c) => c.id)} - style={windowWidth <= BREAKPOINT_MOBILE_LARGE - ? { width: '100%' } - : { width: 'calc((100% - var(--mantine-spacing-md)) * .8)' } - } - /> - </Group> - {(disabledResolution.length === Object.keys(Resolution).length || disabledResolution.includes(null)) && ( - <Text size="xs" c="red" mt={-10}> - Contributions for all resolutions are deactivated for this texture. - </Text> - )} - - <Group gap="md" justify="center" className="w-full" mt="xl"> - <Button - fullWidth - maw="200px" - loading={isPending} - variant="gradient" - gradient={GRADIENT_DANGER} - onClick={handleCancel} - > - Cancel - </Button> - <Button - fullWidth - maw="200px" - loading={isPending} - disabled={!selectedTexture || disabledResolution.length === Object.keys(Resolution).length || disabledResolution.includes(null)} - variant="gradient" - gradient={GRADIENT} - onClick={handleDraftUpdate} - > - Save - </Button> - </Group> - </Stack> - ); -} diff --git a/src/app/(pages)/(protected)/contribute/submit/contribution-tools.tsx b/src/app/(pages)/(protected)/contribute/submit/contribution-tools.tsx deleted file mode 100644 index af740a28..00000000 --- a/src/app/(pages)/(protected)/contribute/submit/contribution-tools.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; - -import { GoArrowLeft, GoDownload, GoTrash, GoUpload } from 'react-icons/go'; - -import { Button, Group, Stack } from '@mantine/core'; -import { useDisclosure } from '@mantine/hooks'; -import { Status } from '@prisma/client'; - -import { Modal } from '~/components/modal'; -import { Tile } from '~/components/tile'; -import { useCurrentUser } from '~/hooks/use-current-user'; -import { useDeviceSize } from '~/hooks/use-device-size'; -import { BREAKPOINT_MOBILE_LARGE, GRADIENT, GRADIENT_DANGER } from '~/lib/constants'; -import { submitContributions } from '~/server/data/contributions'; - -import { ContributionDeleteModal } from './delete-modal'; - -import type { ContributionWithCoAuthorsAndPoll } from '~/types'; - -interface Props { - activeTab: number, - contributions: ContributionWithCoAuthorsAndPoll[]; - onUpdate: () => void; - onSubmitHover: (on: boolean) => void; - onDeleteMode: (on: boolean) => void; - - contributionToDelete: string[]; - setContributionToDelete: (ids: string[]) => void; -} - -export function ContributionTools({ - activeTab, - contributions, - onUpdate, - onDeleteMode, - onSubmitHover, - contributionToDelete, - setContributionToDelete, -}: Props) { - const [windowWidth] = useDeviceSize(); - - const user = useCurrentUser()!; // the user is guaranteed to be logged in (per the layout) - const linkRef = useRef<HTMLAnchorElement>(null); - - const ready = useMemo(() => contributions.filter((c) => c.textureId !== null && c.status === Status.DRAFT), [contributions]); - - const [isDeletionMode, setDeletionMode] = useState(false); - const [isDeleteModalOpened, { open: openDeleteModal, close: closeDeleteModal }] = useDisclosure(false); - - useEffect(() => { - if (!isDeletionMode) setContributionToDelete([]); - onDeleteMode(isDeletionMode); - }, [isDeletionMode, onDeleteMode, setContributionToDelete]); - - const handleContributionsSubmit = () => { - submitContributions(user.id!, contributions.filter((c) => c.textureId !== null && c.status === Status.DRAFT).map((c) => c.id)) - .then(() => onUpdate()); - }; - - const handleContributionsDownload = async () => { - fetch(`/api/download/contributions/${user.id}`, { method: 'GET' }) - .then((response) => response.blob()) - .then((blob) => { - const url = window.URL.createObjectURL(blob); - const link = linkRef.current; - - if (!link) return; - - link.href = url; - link.download = 'contributions.zip'; - link.click(); - window.URL.revokeObjectURL(url); - }); - }; - - return ( - <Tile - maw={windowWidth <= BREAKPOINT_MOBILE_LARGE ? '100%' : 274} - w="100%" - radius={5} - p="xs" - > - <Modal - opened={isDeleteModalOpened} - onClose={closeDeleteModal} - title="Confirmation" - popup - > - <ContributionDeleteModal - contributions={contributions} - contributionToDelete={contributionToDelete} - closeModal={(decision) => { - if (decision === 'yes') onUpdate(); - - setDeletionMode(false); - closeDeleteModal(); - }} - /> - </Modal> - - <Stack gap="xs"> - <Button - variant="gradient" - gradient={GRADIENT} - disabled={contributions.length === 0} - className={contributions.length === 0 ? 'button-disabled-with-bg' : undefined} - onClick={handleContributionsDownload} - justify="space-between" - rightSection={<GoDownload />} - > - Download all contributions - <a ref={linkRef} style={{ display: 'none' }} /> - </Button> - - {!isDeletionMode && ( - <Button - fullWidth - variant="gradient" - gradient={GRADIENT_DANGER} - onClick={() => setDeletionMode(true)} - disabled={contributions.length === 0} - className={contributions.length === 0 ? 'button-disabled-with-bg' : undefined} - justify="space-between" - rightSection={<GoTrash />} - > - Enable deletion mode - </Button> - )} - - {isDeletionMode && ( - <Group gap="xs" wrap="nowrap"> - <Button - variant="default" - className="navbar-icon-fix" - onClick={() => setDeletionMode(false)} - p={0} - > - <GoArrowLeft /> - </Button> - <Button - fullWidth - variant="gradient" - gradient={GRADIENT_DANGER} - onClick={openDeleteModal} - disabled={contributionToDelete.length === 0} - className={contributionToDelete.length === 0 ? 'button-disabled-with-bg' : undefined} - > - Delete {contributionToDelete.length > 1 ? contributionToDelete.length : ''} contribution{contributionToDelete.length > 1 ? 's' : ''} - </Button> - </Group> - )} - - {activeTab === 0 && ( - <Button - variant="gradient" - gradient={GRADIENT} - disabled={ready.length === 0 || isDeletionMode} - className={(ready.length === 0 || isDeletionMode) ? 'button-disabled-with-bg' : undefined} - fullWidth - onClick={() => handleContributionsSubmit()} - onMouseEnter={() => onSubmitHover(true)} - onMouseLeave={() => onSubmitHover(false)} - justify="space-between" - rightSection={<GoUpload />} - > - Submit {ready.length > 1 ? ready.length : ''} draft{ready.length > 1 ? 's' : ''} - </Button> - )} - </Stack> - </Tile> - ); -} diff --git a/src/app/(pages)/(protected)/contribute/submit/delete-modal.tsx b/src/app/(pages)/(protected)/contribute/submit/delete-modal.tsx deleted file mode 100644 index d453092d..00000000 --- a/src/app/(pages)/(protected)/contribute/submit/delete-modal.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Button, Group, Stack, Text } from '@mantine/core'; - -import { useCurrentUser } from '~/hooks/use-current-user'; -import { GRADIENT, GRADIENT_DANGER } from '~/lib/constants'; -import { deleteContributions } from '~/server/data/contributions'; - -import { ContributionPanelItem } from './contribution-item'; - -import type { ContributionWithCoAuthorsAndPoll } from '~/types'; - -export interface ContributionDeleteModalProps { - contributions: ContributionWithCoAuthorsAndPoll[]; - contributionToDelete: string[]; - closeModal: (decision: 'yes' | 'no') => void; -} - -export function ContributionDeleteModal({ contributions, contributionToDelete, closeModal }: ContributionDeleteModalProps) { - const userId = useCurrentUser()!.id!; - - const handleContributionsDelete = async () => { - await deleteContributions(userId, contributionToDelete); - closeModal('yes'); - }; - - return ( - <Stack gap="md"> - <Stack gap={0}> - <Text size="sm">Are you sure you want to delete {contributionToDelete.length} contribution{contributionToDelete.length > 1 ? 's' : ''} ?</Text> - <Text size="sm" c="red"> - Please note that this action is irreversible and the contribution{contributionToDelete.length > 1 ? 's' : ''} will be permanently deleted. - </Text> - </Stack> - <Stack> - <Text> - Contribution{contributionToDelete.length > 1 ? 's' : ''} to delete : - </Text> - <Group gap="sm"> - {contributionToDelete.map((id) => - <ContributionPanelItem - key={id} - contribution={contributions.find((c) => c.id === id)!} - /> - )} - </Group> - </Stack> - <Group gap="sm" wrap="nowrap"> - <Button - variant="gradient" - gradient={GRADIENT} - onClick={() => closeModal('no')} - fullWidth - > - No - </Button> - <Button - variant="gradient" - gradient={GRADIENT_DANGER} - onClick={handleContributionsDelete} - fullWidth - > - Yes - </Button> - </Group> - </Stack> - ); -} diff --git a/src/app/(pages)/(protected)/contribute/submit/page.tsx b/src/app/(pages)/(protected)/contribute/submit/page.tsx deleted file mode 100644 index a7e1b940..00000000 --- a/src/app/(pages)/(protected)/contribute/submit/page.tsx +++ /dev/null @@ -1,278 +0,0 @@ -'use client'; - -import { useState, useTransition } from 'react'; - -import { Button, Code, FloatingIndicator, Group, Indicator, Select, Stack, Text } from '@mantine/core'; -import { Dropzone } from '@mantine/dropzone'; -import { useDisclosure } from '@mantine/hooks'; -import { Resolution, Status } from '@prisma/client'; - -import { Modal } from '~/components/modal'; -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, COLORS } from '~/lib/constants'; -import { createRawContributions, getContributionsOfUser, getCoSubmittedContributions } from '~/server/data/contributions'; -import { getTexturesWithUsePaths } from '~/server/data/texture'; - -import { CoAuthorsSelector } from './co-authors-select'; -import { ContributionPanelItem } from './contribution-item'; -import { ContributionModal } from './contribution-modal'; -import { ContributionTools } from './contribution-tools'; - -import type { GetTexturesWithUsePaths } from '~/server/data/texture'; -import type { ContributionWithCoAuthorsAndPoll, PublicUser } from '~/types'; - -import './submit.scss'; - -const SubmitPage = () => { - const user = useCurrentUser()!; // the user is guaranteed to be logged in (per the layout) - - const [isPending, startTransition] = useTransition(); - const [windowWidth] = useDeviceSize(); - - const [resolution, setResolution] = useState<Resolution>(Resolution.x32); - const [selectedCoAuthors, setSelectedCoAuthors] = useState<PublicUser[]>([]); - - const [contributions, setContributions] = useState<ContributionWithCoAuthorsAndPoll[]>([]); - const [coContributions, setCoContributions] = useState<ContributionWithCoAuthorsAndPoll[]>([]); - - const [isModalContributionOpened, { open: openContributionModal, close: closeContributionModal }] = useDisclosure(false); - const [modalContribution, setModalContribution] = useState<ContributionWithCoAuthorsAndPoll | null>(null); - - const [isHoveringSubmit, setHoveringSubmit] = useState(false); - const [isDeletionMode, setDeletionMode] = useState(false); - - const [contributionToDelete, setContributionToDelete] = useState<string[]>([]); - - const [textures, setTextures] = useState<GetTexturesWithUsePaths[]>([]); - - useEffectOnce(() => { - reload(); - }); - - const reload = () => { - startTransition(() => { - getContributionsOfUser(user.id!).then(setContributions); - getCoSubmittedContributions(user.id!).then(setCoContributions); - getTexturesWithUsePaths().then(setTextures); - }); - }; - - const handleFilesDrop = (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) - .then((duplicates) => { - if (duplicates.length > 0) console.error(duplicates.join('\n')); - - getContributionsOfUser(user.id!).then(setContributions); - setActiveTab(0); - }); - }); - }; - - const handleContributionClick = (c: ContributionWithCoAuthorsAndPoll) => { - if (isDeletionMode) { - if (contributionToDelete.includes(c.id)) setContributionToDelete(contributionToDelete.filter((id) => id !== c.id)); - else setContributionToDelete([...contributionToDelete, c.id]); - } - else { - setModalContribution(c); - openContributionModal(); - } - }; - - const getBorderStyles = (c: ContributionWithCoAuthorsAndPoll) => { - if (contributionToDelete.includes(c.id)) - return { boxShadow: '0 0 0 2px var(--mantine-color-red-filled)' }; - - if (c.textureId !== null && isHoveringSubmit && c.status === Status.DRAFT) - return { boxShadow: '0 0 0 2px var(--mantine-color-teal-filled)' }; - }; - - const [groupRef, setGroupRef] = useState<HTMLDivElement | null>(null); - const [controlsRefs, setControlsRefs] = useState<Record<string, HTMLButtonElement | null>>({}); - const [activeTab, setActiveTab] = useState(0); - - const setControlRef = (index: number) => (node: HTMLButtonElement) => { - controlsRefs[index] = node; - setControlsRefs(controlsRefs); - }; - - const controls = Object.entries(Status).map(([key, value], index) => { - const isLast = index === Object.keys(Status).length - 1; - - return ( - <Button - key={key} - ref={setControlRef(index)} - onClick={() => setActiveTab(index)} - mod={{ active: activeTab === index }} - - pl="md" - pr="sm" - fullWidth - variant="filled" - leftSection={<> - <Indicator color={COLORS[value]} mr="md" /> - {value === Status.DRAFT - ? 'Drafted' - : value === Status.PENDING - ? 'Reviewed' - : value.charAt(0).toUpperCase() + value.slice(1).toLowerCase() - } - </>} - rightSection={contributions?.filter((c) => c.status === value).length ?? 0} - justify="space-between" - className="slider-button" - style={windowWidth > BREAKPOINT_MOBILE_LARGE - ? { - borderRight: !isLast && activeTab !== index + 1 && activeTab !== index - ? 'calc(0.0625rem * var(--mantine-scale)) solid var(--mantine-color-default-border)' - : undefined, - borderRadius: isLast ? '0 calc(0.25rem * var(--mantine-scale)) calc(0.25rem * var(--mantine-scale)) 0' : undefined, - } - : undefined - } - /> - ); - }); - - return ( - <Stack gap="md"> - <Modal - forceFullScreen - opened={isModalContributionOpened} - onClose={closeContributionModal} - > - {modalContribution && ( - <ContributionModal - contribution={modalContribution} - textures={textures} - onClose={reload} - /> - )} - </Modal> - - <Stack gap="md"> - <Group gap="sm"> - <Select - label="Resolution" - data={Object.keys(Resolution)} - checkIconPosition="right" - allowDeselect={false} - defaultValue={Resolution.x32} - onChange={(value) => setResolution(value as Resolution)} - style={windowWidth <= BREAKPOINT_MOBILE_LARGE ? { width: '100%' } : { width: 'calc((100% - var(--mantine-spacing-md)) * .2)' }} - required - /> - <CoAuthorsSelector - author={user} - onCoAuthorsSelect={setSelectedCoAuthors} - mb={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'xs' : '0'} - style={windowWidth <= BREAKPOINT_MOBILE_LARGE - ? { width: '100%' } - : { width: 'calc((100% - var(--mantine-spacing-md)) * .8)' } - } - /> - </Group> - <Stack gap="2"> - <Text size="sm" fw={500}>Files</Text> - <Dropzone - onDrop={handleFilesDrop} - accept={['image/png']} - loading={isPending} - mt="0" - > - <div> - <Text size="l" inline> - Drag <Code>.PNG</Code> files here or click to select files - </Text> - <Text size="sm" c="dimmed" inline mt={7}> - Attach as many files as you like, each file will be added as a separate contribution - based on the settings above. - </Text> - </div> - </Dropzone> - <Text size="xs" c="dimmed" fs="italic"> - Please do not submit textures for unsupported mod/modpack. Ask a council member to add them first. - </Text> - </Stack> - </Stack> - - <Group wrap={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'wrap' : 'nowrap'} align="start"> - <ContributionTools - activeTab={activeTab} - contributions={contributions} - onUpdate={reload} - onSubmitHover={setHoveringSubmit} - onDeleteMode={setDeletionMode} - - contributionToDelete={contributionToDelete} - setContributionToDelete={setContributionToDelete} - /> - - <Group w="100%"> - <Stack w="100%"> - <Button.Group - ref={setGroupRef} - style={{ - position: 'relative', - }} - orientation={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'vertical' : 'horizontal'} - > - {controls} - - <FloatingIndicator - target={controlsRefs[activeTab]} - parent={groupRef} - style={{ - border: 'calc(0.0625rem * var(--mantine-scale)) solid #fff3', - borderRadius: (() => { - if (activeTab === 0) return 'calc(0.25rem * var(--mantine-scale)) 0 0 calc(0.25rem * var(--mantine-scale))'; - if (activeTab === Object.keys(Status).length - 1) return '0 calc(0.25rem * var(--mantine-scale)) calc(0.25rem * var(--mantine-scale)) 0'; - return '0'; - })(), - backgroundColor: '#0002', - cursor: 'pointer', - zIndex: 200, - }} - /> - - </Button.Group> - - <Group mb="sm"> - {contributions.filter((c) => c.status === Object - .values(Status)[activeTab]) - .map((contribution) => ( - <ContributionPanelItem - key={contribution.id} - contribution={contribution} - onClick={handleContributionClick} - styles={getBorderStyles(contribution)} - /> - )) - } - {coContributions.filter((c) => c.status === Object - .values(Status)[activeTab]) - .map((contribution) => ( - <ContributionPanelItem - key={contribution.id} - contribution={contribution} - onClick={handleContributionClick} - styles={getBorderStyles(contribution)} - /> - )) - } - </Group> - </Stack> - </Group> - </Group> - </Stack> - ); -}; - -export default SubmitPage; diff --git a/src/app/(pages)/(protected)/council/submissions/page.tsx b/src/app/(pages)/(protected)/council/submissions/page.tsx index 5a93b6be..797464ab 100644 --- a/src/app/(pages)/(protected)/council/submissions/page.tsx +++ b/src/app/(pages)/(protected)/council/submissions/page.tsx @@ -17,7 +17,8 @@ import { getCounselors } from '~/server/data/user'; import { CouncilContributionItem } from './contribution-item'; import type { Texture } from '@prisma/client'; -import type { ContributionWithCoAuthorsAndFullPoll, PublicUser } from '~/types'; +import type { GetPendingContributions } from '~/server/data/contributions'; +import type { PublicUser } from '~/types'; const CouncilContributionsPanel = () => { const [textures, setTextures] = useState<Texture[]>([]); @@ -27,8 +28,8 @@ const CouncilContributionsPanel = () => { const [showVoted, setShowVoted] = useState(false); const [counselors, setCounselors] = useState<PublicUser[]>([]); - const [counselorVoted, setCounselorVoted] = useState<ContributionWithCoAuthorsAndFullPoll[]>([]); - const [counselorUnvoted, setCounselorUnvoted] = useState<ContributionWithCoAuthorsAndFullPoll[]>([]); + const [counselorVoted, setCounselorVoted] = useState<GetPendingContributions[]>([]); + const [counselorUnvoted, setCounselorUnvoted] = useState<GetPendingContributions[]>([]); const counselor = useCurrentUser()!; @@ -50,8 +51,8 @@ const CouncilContributionsPanel = () => { const loadContributions = () => { getPendingContributions() .then((res) => { - const unvoted: ContributionWithCoAuthorsAndFullPoll[] = []; - const voted: ContributionWithCoAuthorsAndFullPoll[] = []; + const unvoted: GetPendingContributions[] = []; + const voted: GetPendingContributions[] = []; res.forEach((c) => { if (c.poll.downvotes.find((dv) => dv.id === counselor.id) || c.poll.upvotes.find((uv) => uv.id === counselor.id)) diff --git a/src/app/(pages)/mods/[modId]/gallery/page.tsx b/src/app/(pages)/mods/[modId]/gallery/page.tsx index 4c878fb0..13ded730 100644 --- a/src/app/(pages)/mods/[modId]/gallery/page.tsx +++ b/src/app/(pages)/mods/[modId]/gallery/page.tsx @@ -18,7 +18,7 @@ import { getModVersionFromMod } from '~/server/data/mods-version'; import { getTexturesFromModVersion } from '~/server/data/texture'; import type { ModVersion, Texture } from '@prisma/client'; -import type { ContributionWithCoAuthors } from '~/types'; +import type { GetLatestContributionsOfModVersion } from '~/server/data/contributions'; export default function ModGalleryPage() { const [resolution, setResolution] = useState<Resolution | 'x16'>(Resolution['x32']); @@ -32,7 +32,7 @@ export default function ModGalleryPage() { const [texturesFiltered, setTexturesFiltered] = useState<Texture[]>([]); const [texturesShown, setTexturesShown] = useState<Texture[][]>([[]]); - const [contributions, setContributions] = useState<ContributionWithCoAuthors[]>([]); + const [contributions, setContributions] = useState<GetLatestContributionsOfModVersion[]>([]); const [texturesShownPerPage, setTexturesShownPerPage] = useState<string>('96'); const [texturesShownPerRow, setTexturesShownPerRow] = useState<number>(12); diff --git a/src/auth.config.ts b/src/auth.config.ts index 7d85b1a2..d90d5897 100644 --- a/src/auth.config.ts +++ b/src/auth.config.ts @@ -12,7 +12,7 @@ export default { clientSecret: process.env.GITHUB_CLIENT_SECRET, authorization: { params: { - scope: 'read:user user:email public_repo', + scope: 'read:user user:email public_repo delete_repo', }, }, }), diff --git a/src/components/small-tile.tsx b/src/components/small-tile.tsx index 3e3b758d..d4311d20 100644 --- a/src/components/small-tile.tsx +++ b/src/components/small-tile.tsx @@ -9,7 +9,7 @@ export function SmallTile({ children, style, ...props }: PolymorphicComponentPro mih={28} style={{ padding: '5px 8px 6px 8px', - borderRadius: 5, + borderRadius: 0, ...style, }} {...props} diff --git a/src/components/texture-contribution.tsx b/src/components/texture-contribution.tsx index 96c39d79..24d5a44c 100644 --- a/src/components/texture-contribution.tsx +++ b/src/components/texture-contribution.tsx @@ -12,16 +12,16 @@ import { useEffectOnce } from '~/hooks/use-effect-once'; import { getVanillaTextureSrc } from '~/lib/utils'; import { getLatestVanillaTextureContribution } from '~/server/actions/faithful-pack'; -import type { Resolution, Texture } from '@prisma/client'; -import type { ContributionWithCoAuthors, FPStoredContribution } from '~/types'; +import type { Contribution, Resolution, Texture } from '@prisma/client'; +import type { FPStoredContribution, Prettify, PublicUser } from '~/types'; -export interface GalleryTextureWithContributionProps { +interface Props { container: RefObject<HTMLDivElement>; rowItemsGap: number; rowItemsLength: number; texture: Texture; resolution: Resolution | 'x16'; - contribution?: ContributionWithCoAuthors; + contribution?: Prettify<Contribution & { owner: PublicUser, coAuthors: PublicUser[] }>; } export function GalleryTextureWithContribution({ @@ -31,7 +31,7 @@ export function GalleryTextureWithContribution({ resolution, texture, contribution, -}: GalleryTextureWithContributionProps) { +}: Props) { const src = useMemo(() => { if (resolution === 'x16') return texture.filepath; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 220b90b0..04e9d4c5 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -107,4 +107,39 @@ export const COLORS: Record<Status, MantineColor> = { export const GITHUB_ORG_NAME = 'faithful-mods'; export const GITHUB_DEFAULT_REPO_NAME = 'resources-default'; -export const FILE_GIT = `https://raw.githubusercontent.com/${GITHUB_ORG_NAME}/${GITHUB_DEFAULT_REPO_NAME}/${process.env.NODE_ENV === 'production' ? 'main' : 'dev'}`; + +export type RawUrl = + | `https://raw.githubusercontent.com/${string}/${string}/${string}` + | `https://raw.githubusercontent.com/${string}/${string}/${string}/${string}` + +export type FileGitParams = { + orgOrUser: string; + repository: string; + branchOrCommit?: string; + path?: string; +} + +export const gitRawUrl = ({ orgOrUser, repository, branchOrCommit, path }: FileGitParams): RawUrl => { + if (branchOrCommit) return `https://raw.githubusercontent.com/${orgOrUser}/${repository}/${branchOrCommit}/${path}`; + const branch = process.env.NODE_ENV === 'production' ? 'main' : 'dev'; + + return `https://raw.githubusercontent.com/${orgOrUser}/${repository}/${branch}/${path}`; +}; + +export type CommitUrl = `https://github.com/${string}/${string}/commit/${string}`; + +export type GitCommitUrlParams = { + orgOrUser: string; + repository: string; + commitSha: string; +} + +export const gitCommitUrl = ({ orgOrUser, repository, commitSha }: GitCommitUrlParams): CommitUrl => { + return `https://github.com/${orgOrUser}/${repository}/commit/${commitSha}`; +}; + +export type BlobUrl = `https://github.com/${string}/${string}/blob/${string}/${string}`; + +export const gitBlobUrl = ({ orgOrUser, repository, branchOrCommit, path }: FileGitParams): BlobUrl => { + return `https://github.com/${orgOrUser}/${repository}/blob/${branchOrCommit}/${path}`; +}; diff --git a/src/server/actions/files.ts b/src/server/actions/files.ts index 83650953..8eca11b8 100644 --- a/src/server/actions/files.ts +++ b/src/server/actions/files.ts @@ -8,7 +8,7 @@ import { join } from 'path'; import TOML from '@ltd/j-toml'; import unzipper from 'unzipper'; -import { FILE_DIR, FILE_GIT, FILE_PATH } from '~/lib/constants'; +import { FILE_DIR, gitRawUrl, FILE_PATH, GITHUB_ORG_NAME, GITHUB_DEFAULT_REPO_NAME } from '~/lib/constants'; import { db } from '~/lib/db'; import { calculateHash } from '~/lib/hash'; import { socket } from '~/lib/serversocket'; @@ -345,7 +345,7 @@ export async function extractDefaultResourcePack(jar: File, modVersion: ModVersi mcmeta, }); - const filepath = `${FILE_GIT}/${texture.id}.png` as const; + const filepath = `${gitRawUrl({ orgOrUser: GITHUB_ORG_NAME, repository: GITHUB_DEFAULT_REPO_NAME })}/${texture.id}.png` as const; await db.texture.update({ where: { id: texture.id }, data: { filepath } }); filesToCommit.push(buffer.toString('base64') as base64); diff --git a/src/server/actions/git.ts b/src/server/actions/git.ts index f52dc4f3..95ea9619 100644 --- a/src/server/actions/git.ts +++ b/src/server/actions/git.ts @@ -2,46 +2,268 @@ import 'server-only'; import { Octokit } from '@octokit/rest'; -import { UserRole } from '@prisma/client'; +import { Resolution, UserRole } from '@prisma/client'; import { auth } from '~/auth'; import { canAccess } from '~/lib/auth'; -import { GITHUB_DEFAULT_REPO_NAME, GITHUB_ORG_NAME } from '~/lib/constants'; +import { gitRawUrl, GITHUB_DEFAULT_REPO_NAME, GITHUB_ORG_NAME } from '~/lib/constants'; import { db } from '~/lib/db'; +import type { RestEndpointMethodTypes } from '@octokit/rest'; +import type { RawUrl } from '~/lib/constants'; import type { base64 } from '~/types'; -export async function uploadToRepository(files: base64[], filenames: string[], commitMessage: string): Promise<void> { - await canAccess(UserRole.COUNCIL); +// Singleton instance of octokit + +declare global { + var octokit: Octokit | undefined; +} + +let octokit = globalThis.octokit || undefined; +if (process.env.NODE_ENV !== 'production') globalThis.octokit = octokit; + +async function getOctokit() { + if (octokit) return octokit; const session = await auth(); const user = session?.user!; // We know the user is logged in because of the canAccess check // Authenticate with GitHub API using current logged user's access token const userToken = await db.account.findFirstOrThrow({ where: { userId: user.id }, select: { access_token: true } }); - const octokit = new Octokit({ + octokit = new Octokit({ auth: userToken.access_token, }); + return octokit; +} + +// --- End singletons --- + +/** + * Check if the user has a fork of the default repository + */ +export async function getFork() { + const octokit = await getOctokit(); + const username = await getUserGitHubUsername(); + + let fork: RestEndpointMethodTypes['repos']['get']['response']; + try { + fork = await octokit.repos.get({ + owner: username, + repo: GITHUB_DEFAULT_REPO_NAME, + }); + } catch { + return null; + } + + return fork.data.html_url; +} + +/** + * Delete the forked repository from the logged user's account + */ +export async function deleteFork() { + const octokit = await getOctokit(); + const username = await getUserGitHubUsername(); + + await octokit.repos.delete({ + owner: username, + repo: GITHUB_DEFAULT_REPO_NAME, + }); +} + +/** + * Fork the default textures repository to the user's account and wait for the fork to be ready + */ +export async function forkRepository() { + const octokit = await getOctokit(); + + // Fork the repository + await octokit.repos.createFork({ + owner: GITHUB_ORG_NAME, + repo: GITHUB_DEFAULT_REPO_NAME, + }); + + let forkReady = false; + const username = await getUserGitHubUsername(); + + // wait for the fork to be ready before doing anything else + while (!forkReady) { + const fork = await octokit.repos.get({ + owner: username, + repo: GITHUB_DEFAULT_REPO_NAME, + }); + + if (fork.status === 200) forkReady = true; + + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + + // create empty branches for each resolution + const resolutions = Object.keys(Resolution) as Resolution[]; + const firstCommitSha = await getFirstCommit(username, GITHUB_DEFAULT_REPO_NAME); + + for (const resolution of resolutions) { + await createBranchFromCommit(username, GITHUB_DEFAULT_REPO_NAME, resolution, firstCommitSha); + } + + // rename the dev/main branch to x16 (if prod => main => x16 and DEL dev, if dev => dev => x16 and DEL main) + if (process.env.NODE_ENV !== 'production') { + await setDefaultBranch(username, GITHUB_DEFAULT_REPO_NAME, 'dev'); + await deleteBranch(username, GITHUB_DEFAULT_REPO_NAME, 'main'); + await renameBranch(username, GITHUB_DEFAULT_REPO_NAME, 'dev', 'x16'); + } + else { + await deleteBranch(username, GITHUB_DEFAULT_REPO_NAME, 'dev'); + await renameBranch(username, GITHUB_DEFAULT_REPO_NAME, 'main', 'x16'); + } +} + +/** + * Upload the given files to the default textures repository + * @param files the files to upload as base64 + * @param filenames the names of the files (must match the order of the files) + * @param commitMessage the commit message to use + */ +export async function uploadToRepository(files: base64[], filenames: string[], commitMessage: string): Promise<void> { + await canAccess(UserRole.COUNCIL); + const branch = process.env.NODE_ENV === 'production' ? 'main' : 'dev'; // get latest commit - const currentCommit = await getCurrentCommit(octokit, branch); + const currentCommit = await getCurrentCommit(branch); // create blobs for each file - const filesBlobs = await Promise.all(files.map((file) => createBlobFile(octokit, file))); + const blobs = await Promise.all(files.map((file) => createBlobFile(file))); // create new tree with the blobs - const newTree = await createNewTree(octokit, filenames, filesBlobs, currentCommit.tree_sha); + const newTree = await createNewTree({ filenames, blobs, parentTreeSha: currentCommit.tree_sha }); // create a new commit with the new tree - const newCommit = await createCommit(octokit, commitMessage, newTree.sha, currentCommit.commit_sha); + const newCommit = await createCommit({ message: commitMessage, treeSha: newTree.sha, parentCommitSha: currentCommit.commit_sha }); // update the branch to point to the new commit - await setBranchToCommit(octokit, newCommit.sha, branch); + await setBranchToCommit(newCommit.sha, branch); +} + +export interface GitFile { + url: RawUrl; + path: string; + mode: string; + type: string; + sha: string; + size: number; +} + +export async function getContributionsOfFork(resolution: Resolution): Promise<GitFile[]> { + const username = await getUserGitHubUsername(); + const files = await listFilesInBranch(username, GITHUB_DEFAULT_REPO_NAME, resolution); + console.log(files, resolution); + + return files as GitFile[]; } -async function setBranchToCommit(octokit: Octokit, commitSha: string, branch: string) { +async function listFilesInBranch(owner: string, repo: string, branch: string) { + const octokit = await getOctokit(); + const { data } = await octokit.git.getRef({ + owner, + repo, + ref: `heads/${branch}`, + }); + + const commitSha = data.object.sha; + + // Get the tree structure of the commit + try { + const treeResponse = await octokit.git.getTree({ + owner, + repo, + tree_sha: commitSha, + recursive: 'true', // Set to "true" to list all files recursively + }); + + return treeResponse.data.tree + .filter((item) => item.type === 'blob') + .map((item) => ({ + ...item, + url: gitRawUrl({ orgOrUser: owner, repository: repo, branchOrCommit: commitSha, path: item.path }), + })); + } + catch { + return []; + } + +} + +/** + * Get the GitHub username of the currently logged user + */ +async function getUserGitHubUsername() { + const octokit = await getOctokit(); + const { data } = await octokit.users.getAuthenticated(); + return data.login; +} + +async function setDefaultBranch(owner: string, repo: string, branch: string) { + const octokit = await getOctokit(); + await octokit.repos.update({ + owner, + repo, + default_branch: branch, + }); +} + +async function renameBranch(owner: string, repo: string, oldBranch: string, newBranch: string) { + const octokit = await getOctokit(); + const { data } = await octokit.git.getRef({ + owner, + repo, + ref: `heads/${oldBranch}`, + }); + + await octokit.git.createRef({ + owner, + repo, + ref: `refs/heads/${newBranch}`, + sha: data.object.sha, + }); + + await deleteBranch(owner, repo, oldBranch); +} + +async function deleteBranch(owner: string, repo: string, branch: string) { + const octokit = await getOctokit(); + await octokit.git.deleteRef({ + owner, + repo, + ref: `heads/${branch}`, + }); +} + +async function createBranchFromCommit(owner: string, repo: string, branch: string, commitSha: string) { + const octokit = await getOctokit(); + await octokit.git.createRef({ + owner, + repo, + ref: `refs/heads/${branch}`, + sha: commitSha, + }); +} + +async function getFirstCommit(owner: string, repo: string) { + const octokit = await getOctokit(); + const { data } = await octokit.repos.listCommits({ + owner, repo, + sha: 'main', + per_page: 1, + page: 1, + }); + + return data[0]!.sha; +} + +async function setBranchToCommit(commitSha: string, branch: string) { + const octokit = await getOctokit(); await octokit.git.updateRef({ owner: GITHUB_ORG_NAME, repo: GITHUB_DEFAULT_REPO_NAME, @@ -50,22 +272,51 @@ async function setBranchToCommit(octokit: Octokit, commitSha: string, branch: st }); } -async function createCommit(octokit: Octokit, message: string, treeSha: string, parentCommitSha: string) { +interface CreateCommitOptions { + message: string; + treeSha: string; + parentCommitSha?: string; + owner?: string; + repo?: string; +} + +async function createCommit({ message, treeSha, parentCommitSha, owner, repo }: CreateCommitOptions) { + const octokit = await getOctokit(); const { data } = await octokit.git.createCommit({ - owner: GITHUB_ORG_NAME, - repo: GITHUB_DEFAULT_REPO_NAME, + owner: owner ?? GITHUB_ORG_NAME, + repo: repo ?? GITHUB_DEFAULT_REPO_NAME, message, tree: treeSha, - parents: [parentCommitSha], + parents: parentCommitSha ? [parentCommitSha] : [], }); return data; } +async function createEmptyTree(owner = GITHUB_ORG_NAME, repo = GITHUB_DEFAULT_REPO_NAME) { + const octokit = await getOctokit(); + const { data } = await octokit.git.createTree({ + owner, + repo, + tree: [], + }); + + return data.sha; +} + +interface CreateNewTreeOptions { + filenames: string[], + blobs: { url: string, sha: string }[], + parentTreeSha: string + owner?: string; + repo?: string; +} + /** * Create a new tree with the given blobs */ -async function createNewTree(octokit: Octokit, filenames: string[], blobs: { url: string, sha: string }[], parentTreeSha: string) { +async function createNewTree({ filenames, blobs, parentTreeSha, owner, repo }: CreateNewTreeOptions) { + const octokit = await getOctokit(); const tree = blobs.map(({ sha }, index) => { return { path: filenames[index], @@ -76,8 +327,8 @@ async function createNewTree(octokit: Octokit, filenames: string[], blobs: { url }); const { data } = await octokit.git.createTree({ - owner: GITHUB_ORG_NAME, - repo: GITHUB_DEFAULT_REPO_NAME, + owner: owner ?? GITHUB_ORG_NAME, + repo: repo ?? GITHUB_DEFAULT_REPO_NAME, base_tree: parentTreeSha, tree, }); @@ -88,7 +339,8 @@ async function createNewTree(octokit: Octokit, filenames: string[], blobs: { url /** * Prepare a github blob for a file */ -async function createBlobFile(octokit: Octokit, file: base64) { +async function createBlobFile(file: base64) { + const octokit = await getOctokit(); const blobData = await octokit.git.createBlob({ owner: GITHUB_ORG_NAME, repo: GITHUB_DEFAULT_REPO_NAME, @@ -102,7 +354,8 @@ async function createBlobFile(octokit: Octokit, file: base64) { /** * Fetch the current commit of the given repository and branch */ -async function getCurrentCommit(octokit: Octokit, branch: string) { +async function getCurrentCommit(branch: string) { + const octokit = await getOctokit(); const { data: refData } = await octokit.git.getRef({ owner: GITHUB_ORG_NAME, repo: GITHUB_DEFAULT_REPO_NAME, diff --git a/src/server/data/contributions.ts b/src/server/data/contributions.ts index c5bcadeb..cae82b66 100644 --- a/src/server/data/contributions.ts +++ b/src/server/data/contributions.ts @@ -5,60 +5,51 @@ import { Status, UserRole } from '@prisma/client'; import { canAccess } from '~/lib/auth'; import { db } from '~/lib/db'; -import { calculateHash } from '~/lib/hash'; import { getCounselors } from './user'; -import { remove, upload } from '../actions/files'; +import type { GitFile } from '../actions/git'; import type { Contribution, Resolution } from '@prisma/client'; -import type { - ContributionWithCoAuthors, - ContributionWithCoAuthorsAndFullPoll, - ContributionWithCoAuthorsAndPoll, - TextureMCMETA, -} from '~/types'; +import type { Prettify, PublicUser } from '~/types'; // GET -export async function getContributionsOfUser(ownerId: string): Promise<ContributionWithCoAuthorsAndPoll[]> { - await canAccess(UserRole.ADMIN, ownerId); +export type GetContributionsOfUser = Prettify<Contribution & { + coAuthors: PublicUser[], + owner: PublicUser, + poll: { + upvotes: PublicUser[], + downvotes: PublicUser[], + } +}> - return await db.contribution.findMany({ - where: { ownerId }, - include: { - coAuthors: { select: { id: true, name: true, image: true } }, - owner: { select: { id: true, name: true, image: true } }, - poll: true, - }, - }); -} +/** + * Get all contributions of a user, including the co-authors and the poll + */ +export async function getContributionsOfUser(ownerId: string, resolution: Resolution): Promise<GetContributionsOfUser[]> { + await canAccess(UserRole.ADMIN, ownerId); -export async function getContributionsOfTexture(textureId: number): Promise<ContributionWithCoAuthorsAndPoll[]> { return await db.contribution.findMany({ - where: { textureId, status: Status.ACCEPTED }, + where: { ownerId, resolution }, include: { coAuthors: { select: { id: true, name: true, image: true } }, owner: { select: { id: true, name: true, image: true } }, - poll: true, + poll: { select: { downvotes: true, upvotes: true } }, }, }); } -export async function getCoSubmittedContributions(coAuthorId: string): Promise<ContributionWithCoAuthorsAndPoll[]> { - 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 type GetPendingContributions = Prettify<Omit<Contribution, 'status'> & { + status: typeof Status.PENDING, + coAuthors: PublicUser[], + owner: PublicUser, + poll: { + upvotes: PublicUser[], + downvotes: PublicUser[], + } +}> -export async function getPendingContributions(): Promise<ContributionWithCoAuthorsAndFullPoll[]> { +export async function getPendingContributions(): Promise<GetPendingContributions[]> { await canAccess(UserRole.COUNCIL); return await db.contribution.findMany({ @@ -76,16 +67,16 @@ export async function getPendingContributions(): Promise<ContributionWithCoAutho }, }, }, - }); + }) as GetPendingContributions[]; } -export async function findContribution(hash: string): Promise<Contribution | null> { - return db.contribution.findFirst({ - where: { hash }, - }); -} +export type GetLatestContributionsOfModVersion = Prettify<Omit<Contribution, 'status'> & { + status: typeof Status.ACCEPTED, + coAuthors: PublicUser[], + owner: PublicUser, +}> -export async function getLatestContributionsOfModVersion(modVersionId: string, res: Resolution): Promise<ContributionWithCoAuthors[]> { +export async function getLatestContributionsOfModVersion(modVersionId: string, res: Resolution): Promise<GetLatestContributionsOfModVersion[]> { return db.resource.findMany({ where: { modVersionId, @@ -119,135 +110,13 @@ export async function getLatestContributionsOfModVersion(modVersionId: string, r .flatMap((r) => r.linkedTextures) .map((linkedTexture) => linkedTexture.texture) .unique((t1, t2) => t1.id === t2.id) - .map((texture) => texture.contributions[0]) + .map((texture) => texture.contributions[0] as GetLatestContributionsOfModVersion) .filter((c) => !!c) ); } // POST -export async function createRawContributions( - ownerId: string, - coAuthors: string[], - resolution: Resolution, - data: FormData -): Promise<string[]> { - await canAccess(UserRole.ADMIN, ownerId); - - const duplicates: string[] = []; - const files = data.getAll('files') as File[]; - - const contributions: Contribution[] = []; - for (const file of files) { - const buffer = await file.arrayBuffer(); - const hash = calculateHash(Buffer.from(buffer)); - - if (await findContribution(hash)) { - duplicates.push(`Contribution "${file.name}" has already been submitted`); - continue; - } - - 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, - filepath, - filename: file.name, - pollId: poll.id, - hash, - }, - }); - - contributions.push(contribution); - } - - return duplicates; -} - -export async function updateContributionPicture(ownerId: string, contributionId: string, formData: FormData) { - await canAccess(UserRole.ADMIN, ownerId); - - const file = formData.get('file') as File; - const buffer = await file.arrayBuffer(); - const hash = calculateHash(Buffer.from(buffer)); - - if (await findContribution(hash)) throw new Error(`Contribution "${file.name}" has already been submitted`); - const filepath = await upload(file, `textures/contributions/${ownerId}/`); - - const oldFile = await db.contribution.findFirst({ where: { id: contributionId }, select: { filepath: true } }); - if (oldFile) await remove(oldFile.filepath as `/files/${string}`); - - const contribution = await db.contribution.update({ where: { id: contributionId }, data: { filepath, filename: file.name, hash, status: Status.DRAFT } }); - - // reset poll - await db.poll.update({ - where: { id: contribution.pollId }, - data: { - upvotes: { set: [] }, - downvotes: { set: [] }, - }, - }); - - return contribution; -} - -export async function updateDraftContribution({ - ownerId, - contributionId, - coAuthors, - resolution, - textureId, - mcmeta, -}: { - ownerId: string; - contributionId: string; - coAuthors: string[]; - resolution: Resolution; - textureId: number; - mcmeta: TextureMCMETA; -}): Promise<ContributionWithCoAuthors> { - await canAccess(UserRole.ADMIN, ownerId); - - const contribution = await db.contribution.update({ - where: { id: contributionId }, - data: { - coAuthors: { set: coAuthors.map((id) => ({ id })) }, - resolution, - textureId, - mcmeta, - status: Status.DRAFT, - }, - include: { - coAuthors: { select: { id: true, name: true, image: true } }, - owner: { select: { id: true, name: true, image: true } }, - }, - }); - - // reset poll - await db.poll.update({ - where: { id: contribution.pollId }, - data: { - upvotes: { set: [] }, - downvotes: { set: [] }, - }, - }); - - return contribution; -} - -export async function submitContributions(ownerId: string, ids: string[]) { - await canAccess(UserRole.ADMIN, ownerId); - - return await db.contribution.updateMany({ - where: { id: { in: ids }, ownerId }, - data: { status: Status.PENDING }, - }); -} - export async function checkContributionStatus(contributionId: string) { await canAccess(UserRole.COUNCIL); @@ -280,18 +149,52 @@ export async function checkContributionStatus(contributionId: string) { } } -export async function removeCoAuthor(ownerId: string, coAuthorId: string, contributionId: string) { +export async function createContributionsFromGitFiles(ownerId: string, resolution: Resolution, files: GitFile[]) { await canAccess(UserRole.ADMIN, ownerId); - return await db.contribution.update({ - where: { id: contributionId }, - data: { coAuthors: { disconnect: { id: coAuthorId } } }, - }); + for (const file of files) { + const existingContribution = await db.contribution.findFirst({ where: { hash: file.sha } }); + if (existingContribution) continue; + + let textureId: number; + + try { + const filename = file.path.includes('/') ? file.path.split('/')[1] : file.path; + textureId = parseInt(filename?.replace('.png', '') ?? '', 10); + if (isNaN(textureId)) continue; + } catch (e) { + console.error(e); + continue; + } + + const poll = await db.poll.create({ data: {} }); + const contribution = await db.contribution.create({ + data: { + ownerId, + filepath: file.url, + hash: file.sha, + status: Status.DRAFT, + pollId: poll.id, + filename: file.path, + resolution, + textureId, + }, + }); + + // reset poll + await db.poll.update({ + where: { id: contribution.pollId }, + data: { + upvotes: { set: [] }, + downvotes: { set: [] }, + }, + }); + } } // DELETE -export async function deleteContributions( +export async function deleteContributionsOrArchive( ownerId: string, ids: string[] ): Promise<void> { @@ -303,21 +206,16 @@ export async function deleteContributions( }); 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) - ) { + // removed from git without being accepted + if (contribution.status !== Status.ACCEPTED && contribution.status !== Status.ARCHIVED) { + await db.contribution.delete({ where: { id: contribution.id } }); + await db.poll.delete({ where: { id: contribution.pollId } }); + } + else { await db.contribution.update({ where: { id: contribution.id }, - data: { coAuthors: { disconnect: { id: ownerId } } }, + data: { status: Status.ARCHIVED }, }); } - - // Base case: owner wants to delete the contribution - if (contribution.ownerId === ownerId) { - await remove(contribution.filepath as `/files/${string}`); - await db.contribution.delete({ where: { id: contribution.id } }); - } } } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 3582832b..08fdc16d 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,5 +1,4 @@ import type { - Contribution, Mod, Modpack, ModpackVersion, @@ -38,8 +37,8 @@ export interface SocketModUpload { }; } -export type ModpackVersionWithMods = ModpackVersion & { mods: ModVersion[] }; -export type ModVersionExtended = ModVersion & { modpacks: Modpack[], textures: number, linked: number }; +export type ModpackVersionWithMods = Prettify<ModpackVersion & { mods: ModVersion[] }>; +export type ModVersionExtended = Prettify<ModVersion & { modpacks: Modpack[], textures: number, linked: number }>; export type PublicUser = { id: string; @@ -47,14 +46,10 @@ export type PublicUser = { image: string | null; }; -export type FullPoll = Poll & { +export type FullPoll = Prettify<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 ContributionActivationStatus = { /** null means any resolution */ From eb629f8eeb973e05fe13bf2411fce3b42408da43 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Sun, 18 Aug 2024 02:21:50 +0200 Subject: [PATCH 06/30] feat : (part 2) move contribution settings to profile & remove reports --- .../(protected)/contribute/settings/page.tsx | 127 ------ .../(protected)/user/[userId]/page.tsx | 289 ++++++++++--- .../user/[userId]/reports-panel.tsx | 380 ------------------ .../user/[userId]/settings-panel.tsx | 113 ------ 4 files changed, 244 insertions(+), 665 deletions(-) delete mode 100644 src/app/(pages)/(protected)/contribute/settings/page.tsx delete mode 100644 src/app/(pages)/(protected)/user/[userId]/reports-panel.tsx delete mode 100644 src/app/(pages)/(protected)/user/[userId]/settings-panel.tsx diff --git a/src/app/(pages)/(protected)/contribute/settings/page.tsx b/src/app/(pages)/(protected)/contribute/settings/page.tsx deleted file mode 100644 index 71abb7dc..00000000 --- a/src/app/(pages)/(protected)/contribute/settings/page.tsx +++ /dev/null @@ -1,127 +0,0 @@ -'use client'; - -import Link from 'next/link'; - -import { useState, useTransition } from 'react'; - -import { GoCheckCircle, GoStop } from 'react-icons/go'; - -import { Button, Group, Stack, Text } from '@mantine/core'; - -import { Tile } from '~/components/tile'; -import { useEffectOnce } from '~/hooks/use-effect-once'; -import { getFork, deleteFork, forkRepository } from '~/server/actions/git'; - -export default function ContributeSettingsPage() { - const [forked, setHasFork] = useState<string | null>(null); - const [loading, startTransition] = useTransition(); - - useEffectOnce(() => { - reload(); - }); - - const reload = async () => { - startTransition(async () => { - setHasFork(await getFork()); - }); - }; - - const handleSetupForkedRepository = async () => { - startTransition(async () => { - await forkRepository(); - await reload(); - }); - }; - - const handleForkDelete = async () => { - startTransition(async () => { - await deleteFork(); - await reload(); - }); - }; - - const forkedInfo = () => { - if (forked) { - return ( - <Tile p="xs" pl="md" color="teal"> - <Group justify="space-between"> - <Group> - <GoCheckCircle size={20} color="white"/> - <Group gap={3}> - <Text size="sm" c="white">Default textures repository forked: </Text> - <Text size="sm" c="white"><Link href={forked} style={{ color: 'white' }}>{forked}</Link></Text> - </Group> - </Group> - - <Group gap="xs"> - <Button variant="outline" color="white">Sync Fork</Button> - </Group> - </Group> - </Tile> - ); - } - - return ( - <Tile p="xs" pl="md" color="yellow"> - <Group justify="space-between"> - <Group> - <GoStop color="black" size={20} /> - <Group gap="xs"> - <Text size="sm" c="black">Default textures repository not forked</Text> - </Group> - </Group> - - <Group gap="xs"> - <Button - variant="outline" - color="black" - onClick={handleSetupForkedRepository} - disabled={!!forked} - loading={loading} - > - Create Fork - </Button> - </Group> - </Group> - </Tile> - ); - }; - - return ( - <Stack gap="xl"> - <Stack gap="xs"> - <Text fw={700}>General</Text> - {forkedInfo()} - </Stack> - - <Stack gap="xs"> - <Text fw={700}>Danger Zone</Text> - <Tile - p="xs" - pl="md" - withBorder - style={{ - backgroundColor: 'transparent', - borderColor: 'var(--mantine-color-red-filled)', - }} - > - <Group justify="space-between"> - <Stack gap={0}> - <Text>Delete the forked repository</Text> - <Text c="dimmed" size="xs">This action is irreversible, all contributions will be lost.</Text> - </Stack> - <Button - variant="default" - style={{ color: 'var(--mantine-color-red-text)' }} - onClick={handleForkDelete} - disabled={!forked} - loading={loading} - > - Delete Fork - </Button> - </Group> - </Tile> - </Stack> - </Stack> - ); -} diff --git a/src/app/(pages)/(protected)/user/[userId]/page.tsx b/src/app/(pages)/(protected)/user/[userId]/page.tsx index bac32760..59e04f79 100644 --- a/src/app/(pages)/(protected)/user/[userId]/page.tsx +++ b/src/app/(pages)/(protected)/user/[userId]/page.tsx @@ -1,79 +1,278 @@ 'use client'; +import Link from 'next/link'; import { useParams, useRouter } from 'next/navigation'; -import { useState } from 'react'; +import { useState, useTransition } from 'react'; -import { Badge, Button, Group, Tabs } from '@mantine/core'; +import { GoCheckCircle, GoStop } from 'react-icons/go'; + +import { Button, Text, TextInput, Group, Stack, Badge } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { UserRole } from '@prisma/client'; import { signOut } from 'next-auth/react'; +import { useSession } from 'next-auth/react'; +import { TextureImage } from '~/components/texture-img'; import { Tile } from '~/components/tile'; 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, GRADIENT, MAX_NAME_LENGTH, MIN_NAME_LENGTH } from '~/lib/constants'; import { notify } from '~/lib/utils'; -import { getReportsOfUser } from '~/server/data/reports'; +import { deleteFork, forkRepository, getFork } from '~/server/actions/git'; import { getUserById } from '~/server/data/user'; - -import { UserReportsPanel } from './reports-panel'; -import { UserSettingsPanel } from './settings-panel'; +import { updateUser } from '~/server/data/user'; import type { User } from '@prisma/client'; -import type { ReportWithReporter } from '~/types'; const UserPage = () => { const params = useParams(); - const loggedUser = useCurrentUser(); + const user = useCurrentUser()!; const self = params.userId === 'me'; const [displayedUser, setDisplayedUser] = useState<User>(); - const [reports, setReports] = useState<ReportWithReporter[]>([]); const router = useRouter(); + const { update } = useSession(); + const [loading, startTransition] = useTransition(); + const [windowWidth] = useDeviceSize(); + + const [hasFork, setHasFork] = useState<string | null>(null); + + const form = useForm<Pick<User, 'name' | 'image'>>({ + initialValues: { name: user.name!, image: user.image! }, + validate: { + name: (value) => { + if (!value) return 'You must provide a name'; + 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`; + }, + image: (value) => { + if (!value) return null; + if (!value?.startsWith('https://')) return 'Your personal picture should be a HTTPS URL'; + }, + }, + onValuesChange: () => { + form.validate(); + }, + }); + + const onSubmit = (values: typeof form.values) => { + if (!user) return; + + startTransition(() => { + updateUser({ ...values, id: user.id! }) + .then((user) => { + update(user); + notify('Success', 'Profile updated', 'teal'); + }) + .catch((err) => { + console.error(err); + notify('Error', err.message, 'red'); + }); + }); + }; + + const reload = async () => { + startTransition(async () => { + // Avoid logged users to access their own page with their id + if (params.userId === user?.id) router.push('/user/me'); + const userId = self ? user?.id! : params.userId as string; + + getFork().then(setHasFork); + getUserById(userId).then(setDisplayedUser); + }); + }; + + const handleForkDelete = async () => { + startTransition(async () => { + await deleteFork(); + await reload(); + }); + }; + useEffectOnce(() => { - // Avoid logged users to access their own page with their id - if (params.userId === loggedUser?.id) router.push('/user/me'); - - const userId = self ? loggedUser?.id! : params.userId as string; - - getUserById(userId) - .then(setDisplayedUser) - .catch((err: Error) => { - notify('Error', err.message, 'red'); - }); - - getReportsOfUser(userId) - .then(setReports) - .catch((err) => { - console.error(err); - notify('Error', 'Failed to fetch reports', 'red'); - }); + reload(); }); - return (displayedUser && ( - <Tabs defaultValue="1" variant="pills" color="blue" > - <Tile mb="sm"> - <Group justify="space-between"> - <Tabs.List> - <Tabs.Tab value="1">Settings</Tabs.Tab> - <Tabs.Tab value="2" rightSection={reports.length > 0 ? <Badge color="orange">{reports.length}</Badge> : undefined}> - Reports - </Tabs.Tab> - </Tabs.List> - {self && ( + const handleSetupForkedRepository = async () => { + startTransition(async () => { + await forkRepository(); + await reload(); + }); + }; + + const forkedInfo = () => { + if (hasFork) { + return ( + <Tile p="xs" pl={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'xs' : 'md'} color="teal"> + <Group justify="space-between" gap="xs"> + <Group gap="sm"> + <GoCheckCircle size={20} color="white" /> + <Group gap={3}> + <Text size="sm" c="white">Default textures repository forked: </Text> + <Text size="sm" c="white"> + <Link href={hasFork} style={{ color: 'white' }}> + {windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'link' : hasFork} + </Link> + </Text> + </Group> + </Group> + <Button - variant="transparent" - color="red" - onClick={() => signOut({ callbackUrl: '/' })} + variant="outline" + color="white" + fullWidth={windowWidth <= BREAKPOINT_MOBILE_LARGE} > - Logout + Sync Fork </Button> - )} + </Group> + </Tile> + ); + } + + return ( + <Tile p="xs" pl={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'xs' : 'md'} color="yellow"> + <Group justify="space-between" gap="xs"> + <Group gap="sm"> + <GoStop color="black" size={20} /> + <Group gap="xs"> + <Text size="sm" c="black">Default textures repository not forked</Text> + </Group> + </Group> + + <Button + variant="outline" + color="black" + onClick={handleSetupForkedRepository} + disabled={!!hasFork} + loading={loading} + fullWidth={windowWidth <= BREAKPOINT_MOBILE_LARGE} + > + Create Fork + </Button> </Group> </Tile> + ); + }; + + return (displayedUser && ( + <Stack gap="xl"> + <Group + wrap={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'wrap' : 'nowrap'} + gap="xs" + align="start" + justify="center" + > + <Stack w={160} align="center" gap="xs"> + <TextureImage + src={form.values['image'] ?? ''} + alt="User avatar" + styles={{ + borderRadius: 'var(--mantine-radius-default)', + }} + size={160} + /> + + <Badge + color={user?.role === UserRole.BANNED ? 'red' : 'teal'} + variant="filled" + > + {user?.role ?? '?'} + </Badge> + </Stack> + + <Tile w="100%" p="sm" h={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'auto' : 160}> + <Group + h="100%" + justify="space-between" + wrap={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'wrap' : 'nowrap'} + > + <Stack + h="100%" + w="100%" + justify="space-between" + > + <TextInput + label="Name" + required + w="100%" + {...form.getInputProps('name')} + /> + <TextInput + label="Picture URL" + w="100%" + {...form.getInputProps('image')} + /> + </Stack> - <Tabs.Panel value="1"><UserSettingsPanel user={displayedUser} self={self} /></Tabs.Panel> - <Tabs.Panel value="2"><UserReportsPanel user={displayedUser} self={self} reports={reports} /></Tabs.Panel> - </Tabs> + <Stack + h="100%" + w="100%" + justify="space-between" + align="flex-end" + style={{ + flexDirection: windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'column-reverse' : 'column', + }} + > + <Button + justify={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'center' : 'right'} + variant="transparent" + color="red" + onClick={() => signOut({ callbackUrl: '/' })} + > + Sign out + </Button> + <Button + variant="gradient" + gradient={GRADIENT} + onClick={() => onSubmit(form.values)} + disabled={loading || !form.isValid() || user === undefined} + loading={loading} + > + Save + </Button> + </Stack> + </Group> + </Tile> + </Group> + + <Stack gap="xs"> + <Text fw={700}>Contributions Repository</Text> + {forkedInfo()} + </Stack> + + <Stack gap="xs" mb="sm"> + <Text fw={700}>Danger Zone</Text> + <Tile + p="xs" + pl="md" + withBorder + style={{ + backgroundColor: 'transparent', + borderColor: 'var(--mantine-color-red-filled)', + }} + > + <Group justify="space-between"> + <Stack gap={0}> + <Text>Delete the forked repository</Text> + <Text c="dimmed" size="xs">This action is irreversible, all contributions will be lost.</Text> + </Stack> + <Button + variant="default" + style={{ color: 'var(--mantine-color-red-text)' }} + onClick={handleForkDelete} + disabled={!hasFork} + loading={loading} + fullWidth={windowWidth <= BREAKPOINT_MOBILE_LARGE} + > + Delete Fork + </Button> + </Group> + </Tile> + </Stack> + </Stack> )); }; diff --git a/src/app/(pages)/(protected)/user/[userId]/reports-panel.tsx b/src/app/(pages)/(protected)/user/[userId]/reports-panel.tsx deleted file mode 100644 index 40f181c8..00000000 --- a/src/app/(pages)/(protected)/user/[userId]/reports-panel.tsx +++ /dev/null @@ -1,380 +0,0 @@ - -import Link from 'next/link'; - -import { Fragment, startTransition, useEffect, useState } from 'react'; - -import { FaArrowRight } from 'react-icons/fa'; -import { IoCloseOutline, IoCheckmark } from 'react-icons/io5'; - -import { Avatar, Button, Group, Select, Stack, Table, Text, Textarea } from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { UserRole, Status } from '@prisma/client'; - -import { Tile } from '~/components/tile'; -import { useDeviceSize } from '~/hooks/use-device-size'; -import { useEffectOnce } from '~/hooks/use-effect-once'; -import { BREAKPOINT_MOBILE_LARGE, GRADIENT_DANGER, GRADIENT_WARNING } from '~/lib/constants'; -import { notify } from '~/lib/utils'; -import { getReportsReasons, reportSomeone, updateReportStatus } from '~/server/data/reports'; -import { getPublicUsers, updateUserRole } from '~/server/data/user'; - -import type { SelectProps } from '@mantine/core'; -import type { ReportReason, Report, User } from '@prisma/client'; -import type { PublicUser } from '~/types'; - -export function UserReportsPanel({ user, self, reports }: { user: User, reports: Report[], self: boolean }) { - const [users, setUsers] = useState<PublicUser[]>([]); - const [reasons, setReasons] = useState<ReportReason[]>([]); - - const [localUser, setUser] = useState<User>(user); - const [localReports, setReports] = useState<Report[]>(reports); - - useEffect(() => { - setReports(reports); - setUser(user); - }, [reports, user]); - - const [windowWidth] = useDeviceSize(); - - useEffectOnce(() => { - getPublicUsers() - .then((u) => { - setUsers(u.filter((uu) => uu.id !== localUser.id)); - }) - .catch((err) => { - console.error(err); - notify('Error', 'Failed to fetch users', 'red'); - }); - - getReportsReasons() - .then(setReasons) - .catch((err) => { - console.error(err); - notify('Error', 'Failed to fetch report reasons', 'red'); - }); - }); - - const form = useForm<{ reportedId: string, reasonId: string, additionalInfo: string }>({ - initialValues: { reportedId: '', reasonId: '', additionalInfo: '' }, - validate: { - reportedId: (value) => !value && 'You must select a user to report', - reasonId: (value) => !value && 'You must select a reason to report', - }, - onValuesChange: () => { - form.validate(); - }, - }); - - const renderUserSelectOption: SelectProps['renderOption'] = ({ option }) => { - const user = users.find((u) => u.id === option.value); - - return ( - <Group gap="sm" wrap="nowrap"> - <Avatar src={user?.image} size={30} radius="xl" /> - <div> - <Text size="sm">{user?.name ?? option.label}</Text> - </div> - </Group> - ); - }; - - const renderReasonSelectOption: SelectProps['renderOption'] = ({ option }) => { - const reason = reasons.find((r) => r.id === option.value)!; - - return ( - <Stack gap={0}> - <Text size="sm" tt="capitalize">{reason.value}</Text> - <Text size="xs" c="dimmed">{reason.description}</Text> - </Stack> - ); - }; - - const report = () => { - startTransition(() => { - reportSomeone({ - reporterId: localUser.id, - reportedId: form.values.reportedId, - reasonId: form.values.reasonId, - additionalInfo: form.values.additionalInfo, - }) - .then(() => { - form.setValues({ reportedId: '', reasonId: '', additionalInfo: '' }); - notify('Success', 'Report sent successfully', 'green'); - }); - }); - }; - - const updateReport = (reportId: string, status: Status) => { - startTransition(() => { - updateReportStatus(reportId, status) - .then(() => { - notify('Success', 'Report dismissed!', 'green'); - setReports(localReports.map((r) => r.id === reportId ? { ...r, status: status } : r)); - }) - .catch((err) => { - console.error(err); - notify('Error', err.message, 'red'); - }); - }); - }; - - const ban = () => { - startTransition(() => { - updateUserRole(localUser.id, UserRole.BANNED) - .then(() => { - notify('Success', 'User banned!', 'green'); - setUser({ ...localUser, role: UserRole.BANNED }); - }) - .catch((err) => { - console.error(err); - notify('Error', err.message, 'red'); - }); - }); - }; - - const pardon = () => { - startTransition(() => { - updateUserRole(localUser.id, UserRole.USER) - .then(() => { - notify('Success', 'User pardoned!', 'green'); - setUser({ ...localUser, role: UserRole.USER }); - }) - .catch((err) => { - console.error(err); - notify('Error', err.message, 'red'); - }); - }); - }; - - return ( - <Stack gap="sm"> - {self && ( - <Tile> - <Stack gap="md"> - <Stack gap={0}> - <Text size="md" fw={700}>Reports</Text> - <Text size="sm"> - Here you can report someone for breaking our <Text component="a" href="/docs/tos" c="blue" target="_blank">ToS</Text>. If you have any issues, please report them here. - </Text> - </Stack> - <Group gap="md"> - <Select - style={{ width: windowWidth <= BREAKPOINT_MOBILE_LARGE ? '100%' : 'calc((100% - var(--mantine-spacing-md)) * .5)' }} - limit={10} - label="User to report" - placeholder="Select or search a user..." - data={users.map((u) => ({ value: u.id, label: u.name ?? 'Unknown' }))} - renderOption={renderUserSelectOption} - defaultValue={''} - searchable - clearable - required - {...form.getInputProps('reportedId')} - /> - <Select - style={{ width: windowWidth <= BREAKPOINT_MOBILE_LARGE ? '100%' : 'calc((100% - var(--mantine-spacing-md)) * .5)' }} - label="Reason" - placeholder="Select a reason..." - data={reasons.map((r) => ({ value: r.id, label: r.value }))} - renderOption={renderReasonSelectOption} - defaultValue={''} - searchable - clearable - required - {...form.getInputProps('reasonId')} - /> - <Textarea - label="Additional information" - description="Please provide as much information as possible." - placeholder="Write here any additional information you think is relevant..." - className="w-full" - rows={5} - {...form.getInputProps('additionalInfo')} - /> - </Group> - <Group> - <Button - variant="gradient" - gradient={GRADIENT_DANGER} - className={!form.isValid() ? 'button-disabled-with-bg' : ''} - disabled={!form.isValid()} - fullWidth={windowWidth <= BREAKPOINT_MOBILE_LARGE} - onClick={report} - > - Report - </Button> - {form.isValid() && ( - <Text size="sm" ta={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'center' : 'left'}> - By reporting someone, you agree to our <Text component="a" href="/docs/tos" c="blue" target="_blank">Terms of Service</Text>. - </Text> - )} - </Group> - </Stack> - </Tile> - )} - - <Tile> - <Stack gap="md"> - <Stack gap={0}> - <Text size="md" fw={700}>Reports against {self ? 'you' : 'that user'}</Text> - {localReports.length === 0 && ( - <Text size="sm"> - {self - ? 'You don\'t have any reports against you. Keep it up!' - : 'This user doesn\'t have any reports against them.' - } - </Text> - )} - {localReports.length > 0 && ( - <Table striped highlightOnHover withColumnBorders withTableBorder mt="xs"> - <Table.Thead> - {windowWidth > BREAKPOINT_MOBILE_LARGE && ( - <Table.Tr> - {!self && <Table.Th>Reporter</Table.Th>} - <Table.Th>Reason</Table.Th> - <Table.Th>Reported</Table.Th> - <Table.Th>Updated</Table.Th> - <Table.Th>Status</Table.Th> - </Table.Tr> - )} - </Table.Thead> - <Table.Tbody> - {localReports.map((report, i) => ( - <Fragment key={report.id}> - {windowWidth > BREAKPOINT_MOBILE_LARGE && ( - <Table.Tr> - {!self && ( - <Table.Td className="w-[200px]"> - <Group justify="space-between"> - {renderUserSelectOption({ option: { value: report.reporterId, label: '' } })} - <Link href={'/user/' + report.reporterId}> - <Button - variant='transparent' - className="navbar-icon-fix" - > - <FaArrowRight /> - </Button> - </Link> - </Group> - </Table.Td> - )} - <Table.Td className="w-[160px]">{reasons.find((r) => r.id === report.reportReasonId)?.value}</Table.Td> - <Table.Td align="center" className="w-[160px]">{report.createdAt.toLocaleString()}</Table.Td> - <Table.Td align="center" className="w-[160px]">{report.updatedAt.toLocaleString()}</Table.Td> - <Table.Td className="w-[160px]" align="center"> - {!self && report.status === Status.PENDING && ( - <Group gap="xs" justify="center"> - <Button size="xs" className="navbar-icon-fix" onClick={() => updateReport(report.id, Status.REJECTED)}><IoCloseOutline className="w-5 h-5" /></Button> - <Button size="xs" className="navbar-icon-fix" onClick={() => updateReport(report.id, Status.ACCEPTED)}><IoCheckmark className="w-5 h-5" /></Button> - </Group> - )} - {!self && report.status !== Status.PENDING && <Text c="dimmed">{report.status}</Text>} - {self && <Text c="dimmed">{report.status}</Text>} - </Table.Td> - </Table.Tr> - )} - {windowWidth <= BREAKPOINT_MOBILE_LARGE && ( - <> - <Table.Tr> - <Table.Td colSpan={2}> - <Group justify="space-between"> - <Stack gap={0}> - <Text c="dimmed" size="sm">Reason: </Text> - <Text size="sm">{reasons.find((r) => r.id === report.reportReasonId)?.value}</Text> - </Stack> - {!self && report.status === Status.PENDING && ( - <Group gap="xs" justify="center"> - <Button size="xs" className="navbar-icon-fix" onClick={() => updateReport(report.id, Status.REJECTED)}><IoCloseOutline className="w-5 h-5" /></Button> - <Button size="xs" className="navbar-icon-fix" onClick={() => updateReport(report.id, Status.ACCEPTED)}><IoCheckmark className="w-5 h-5" /></Button> - </Group> - )} - {!self && report.status !== Status.PENDING && <Text c="dimmed" size="sm">{report.status}</Text>} - {self && <Text c="dimmed" size="sm">{report.status}</Text>} - </Group> - </Table.Td> - </Table.Tr> - {!self && ( - <Table.Tr> - <Table.Td colSpan={2}> - <Group justify="space-between"> - {renderUserSelectOption({ option: { value: report.reporterId, label: '' } })} - <Link href={'/user/' + report.reporterId}> - <Button - variant='transparent' - className="navbar-icon-fix" - > - <FaArrowRight /> - </Button> - </Link> - </Group> - </Table.Td> - </Table.Tr> - )} - <Table.Tr> - <Table.Td align="center"> - <Text c="dimmed" size="sm">Reported:</Text> - <Text size="sm">{report.createdAt.toLocaleString()}</Text> - </Table.Td> - <Table.Td align="center"> - <Text c="dimmed" size="sm">Updated:</Text> - <Text size="sm">{report.updatedAt.toLocaleString()}</Text> - </Table.Td> - </Table.Tr> - </> - )} - {!self && ( - <Table.Tr> - <Table.Td colSpan={5}> - {report.context.length > 0 - ? ( - <> - <Text size="sm" c="dimmed">Additional information:</Text> - <Text size="sm">{report.context}</Text> - </> - ) - : <Text size="sm" ta="center" c="dimmed">No additional information provided.</Text>} - </Table.Td> - </Table.Tr> - )} - </Fragment> - ))} - </Table.Tbody> - </Table> - )} - - {!self && ( - <Group justify="end"> - {localUser.role !== UserRole.BANNED && ( - <Button - mt="sm" - variant="gradient" - gradient={GRADIENT_DANGER} - disabled={localReports.length === 0} - className={localReports.length === 0 ? 'button-disabled-with-bg' : ''} - onClick={ban} - w={windowWidth <= BREAKPOINT_MOBILE_LARGE ? '100%' : 'auto'} - > - Ban - </Button> - )} - {localUser.role === UserRole.BANNED && ( - <Button - mt="sm" - variant="gradient" - gradient={GRADIENT_WARNING} - disabled={localUser.role !== UserRole.BANNED} - className={localUser.role !== UserRole.BANNED ? 'button-disabled-with-bg' : ''} - onClick={pardon} - w={windowWidth <= BREAKPOINT_MOBILE_LARGE ? '100%' : 'auto'} - > - Pardon - </Button> - )} - </Group> - )} - </Stack> - </Stack> - </Tile> - </Stack> - ); -} diff --git a/src/app/(pages)/(protected)/user/[userId]/settings-panel.tsx b/src/app/(pages)/(protected)/user/[userId]/settings-panel.tsx deleted file mode 100644 index c8ac7251..00000000 --- a/src/app/(pages)/(protected)/user/[userId]/settings-panel.tsx +++ /dev/null @@ -1,113 +0,0 @@ -'use client'; - -import { useTransition } from 'react'; - -import { Button, Text, TextInput, Group, Stack, Badge } from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { UserRole } from '@prisma/client'; -import { useSession } from 'next-auth/react'; - -import { TextureImage } from '~/components/texture-img'; -import { Tile } from '~/components/tile'; -import { useDeviceSize } from '~/hooks/use-device-size'; -import { BREAKPOINT_MOBILE_LARGE, GRADIENT, MAX_NAME_LENGTH, MIN_NAME_LENGTH } from '~/lib/constants'; -import { notify } from '~/lib/utils'; -import { updateUser } from '~/server/data/user'; - -import type { User } from '@prisma/client'; - -export function UserSettingsPanel({ user, self }: { user: User, self: boolean }) { - const { update } = useSession(); - const [isPending, startTransition] = useTransition(); - const [windowWidth] = useDeviceSize(); - - const pictureWidth = windowWidth <= BREAKPOINT_MOBILE_LARGE ? `calc(${windowWidth - 2}px - (2 * var(--mantine-spacing-md)) - (2 * var(--mantine-spacing-sm)) )` : '120px'; - - const form = useForm<Pick<User, 'name' | 'image'>>({ - initialValues: { name: user.name, image: user.image }, - validate: { - name: (value) => { - if (!value) return 'You must provide a name'; - 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`; - }, - image: (value) => { - if (!value) return null; - if (!value?.startsWith('https://')) return 'Your personal picture should be a HTTPS URL'; - }, - }, - onValuesChange: () => { - form.validate(); - }, - }); - - const onSubmit = (values: typeof form.values) => { - if (!user) return; - - startTransition(() => { - updateUser({ ...values, id: user.id }) - .then((user) => { - update(user); - notify('Success', 'Profile updated', 'teal'); - }) - .catch((err) => { - console.error(err); - notify('Error', err.message, 'red'); - }); - }); - }; - - return ( - <Tile mb="sm"> - <Group> - <TextureImage - src={form.values['image'] ?? ''} - alt="User avatar" - size={pictureWidth} - /> - - <Stack - justify="space-between" - style={{ width: windowWidth > BREAKPOINT_MOBILE_LARGE ? `calc(100% - ${pictureWidth} - var(--mantine-spacing-md))` : pictureWidth }} - > - <Group - gap="sm" - align="flex-start" - justify="space-between" - > - <Stack gap={0}> - <Text size="md" fw={700}>Profile Settings</Text> - <Text size="sm">Update {self ? 'your profile' : `${user?.name} profile's` } information</Text> - </Stack> - <Badge color={user?.role === UserRole.BANNED ? 'red' : 'teal'} variant="filled">{user?.role ?? '?'}</Badge> - </Group> - - <Group gap="sm"> - <TextInput - label="Name" - required - style={{ width: windowWidth <= BREAKPOINT_MOBILE_LARGE ? '100%' : 'calc((100% - var(--mantine-spacing-sm)) * .3)' }} - {...form.getInputProps('name')} - /> - <TextInput - label="Picture URL" - style={{ width: windowWidth <= BREAKPOINT_MOBILE_LARGE ? '100%' : 'calc((100% - var(--mantine-spacing-sm)) * .7)' }} - {...form.getInputProps('image')} - /> - </Group> - </Stack> - </Group> - - <Button - variant="gradient" - gradient={GRADIENT} - onClick={() => onSubmit(form.values)} - disabled={isPending || !form.isValid() || user === undefined} - loading={isPending} - mt="md" - > - Save - </Button> - </Tile> - ); -} From 26e97e16e0d737b7c67f703cd3a7d1fe1d1d512e Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Sun, 18 Aug 2024 03:50:27 +0200 Subject: [PATCH 07/30] feat : (part 3) almost finished new contribution system --- .../(protected)/contribute/about/page.tsx | 6 - .../(pages)/(protected)/contribute/layout.tsx | 22 - .../(pages)/(protected)/contribute/page.tsx | 422 +++++++++++++++++- .../contribute/{submissions => }/styles.scss | 0 .../contribute/submissions/page.tsx | 312 ------------- src/server/data/contributions.ts | 9 + 6 files changed, 420 insertions(+), 351 deletions(-) delete mode 100644 src/app/(pages)/(protected)/contribute/about/page.tsx delete mode 100644 src/app/(pages)/(protected)/contribute/layout.tsx rename src/app/(pages)/(protected)/contribute/{submissions => }/styles.scss (100%) delete mode 100644 src/app/(pages)/(protected)/contribute/submissions/page.tsx diff --git a/src/app/(pages)/(protected)/contribute/about/page.tsx b/src/app/(pages)/(protected)/contribute/about/page.tsx deleted file mode 100644 index 089a1d45..00000000 --- a/src/app/(pages)/(protected)/contribute/about/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ - -export default function ContributeAboutPage() { - return ( - 'WIP' - ); -} diff --git a/src/app/(pages)/(protected)/contribute/layout.tsx b/src/app/(pages)/(protected)/contribute/layout.tsx deleted file mode 100644 index d3712e59..00000000 --- a/src/app/(pages)/(protected)/contribute/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -'use client'; - -import { TabsLayout } from '~/components/tabs'; - -interface Props { - children: React.ReactNode; -} - -export default function ContributeLayout({ children }: Props) { - return ( - <TabsLayout - tabs={[ - { value: 'about', label: 'About' }, - { value: 'submissions', label: 'Submissions' }, - { value: 'settings', label: 'Settings' }, - ]} - > - {children} - </TabsLayout> - ); - -} diff --git a/src/app/(pages)/(protected)/contribute/page.tsx b/src/app/(pages)/(protected)/contribute/page.tsx index 8194e6a9..82cd8a33 100644 --- a/src/app/(pages)/(protected)/contribute/page.tsx +++ b/src/app/(pages)/(protected)/contribute/page.tsx @@ -1,18 +1,418 @@ 'use client'; -import { redirect } from 'next/navigation'; +import Link from 'next/link'; -import { useLocalStorage } from '@mantine/hooks'; +import { useCallback, useEffect, useState, useTransition } from 'react'; -const CouncilPage = () => { - const [isAboutShown] = useLocalStorage({ - key: 'faithful-modded-show-contribute-about-page', - defaultValue: true, - getInitialValueInEffect: false, +import { GoCommit, GoHash, GoHourglass, GoQuestion, GoRelFilePath } from 'react-icons/go'; +import { IoReload } from 'react-icons/io5'; +import { LuArrowUpDown } from 'react-icons/lu'; + +import { ActionIcon, Badge, Button, FloatingIndicator, Group, Indicator, Kbd, List, Select, Stack, Text } from '@mantine/core'; +import { useHotkeys, useOs, usePrevious } from '@mantine/hooks'; +import { Resolution, Status } from '@prisma/client'; + +import { SmallTile } from '~/components/small-tile'; +import { TextureImage } from '~/components/texture-img'; +import { Tile } from '~/components/tile'; +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, COLORS, gitBlobUrl, gitCommitUrl, GRADIENT, GRADIENT_DANGER } from '~/lib/constants'; +import { getContributionsOfFork, getFork } from '~/server/actions/git'; +import { createContributionsFromGitFiles, deleteContributionsOrArchive, getContributionsOfUser, submitContributions } from '~/server/data/contributions'; +import { getTextures } from '~/server/data/texture'; + +import type { Texture } from '@prisma/client'; +import type { GitFile } from '~/server/actions/git'; +import type { GetContributionsOfUser } from '~/server/data/contributions'; + +import './styles.scss'; + +export default function ContributeSubmissionsPage() { + const user = useCurrentUser()!; // the user is guaranteed to be logged in (per the layout) + const os = useOs(); + + const [loading, startTransition] = useTransition(); + const [hasFork, setHasFork] = useState<string | null>(null); + const [showHelp, helpShown] = useState(true); + + const [selectedContributions, setSelectedContributions] = useState<string[]>([]); + + const [resolution, setResolution] = useState<Resolution>(Resolution.x32); + const prevRes = usePrevious(resolution); + + const [contributions, setContributions] = useState<GetContributionsOfUser[]>([]); + const [textures, setTextures] = useState<Texture[]>([]); + + const [groupRef, setGroupRef] = useState<HTMLDivElement | null>(null); + const [controlsRefs, setControlsRefs] = useState<Record<string, HTMLButtonElement | null>>({}); + const [activeTab, setActiveTab] = useState(0); + const [windowWidth] = useDeviceSize(); + + useHotkeys([ + ['mod+a', () => setSelectedContributions(contributions.filter((c) => c.status === Object.keys(Status)[activeTab]).map((c) => c.id))], + ]); + + const setControlRef = (index: number) => (node: HTMLButtonElement) => { + controlsRefs[index] = node; + setControlsRefs(controlsRefs); + }; + + const reload = async () => { + startTransition(async () => { + const fork = await getFork(); + if (!fork) return; + + setHasFork(fork); + getContributionsOfFork(resolution).then(updateForkContributions); + }); + }; + + const updateForkContributions = useCallback(async (files: GitFile[]) => { + const contributions = await getContributionsOfUser(user.id!, resolution); + const contributedSha = contributions.map((contribution) => contribution.hash); + + // delete contributions that are not in the fork but are in the database + const missingFiles = contributions.filter((contribution) => !files.some((file) => file.sha === contribution.hash)); + await deleteContributionsOrArchive(user.id!, missingFiles.map((contribution) => contribution.id)); + + // add contributions that are not yet in the database + const newFiles = files.filter((file) => !contributedSha.includes(file.sha)); + await createContributionsFromGitFiles(user.id!, resolution, newFiles); + + const contributionsAfter = await getContributionsOfUser(user.id!, resolution); + setContributions(contributionsAfter); + + }, [user, resolution]); + + useEffectOnce(() => { + reload(); + getTextures().then(setTextures); }); - if (isAboutShown) redirect('/contribute/about'); - redirect('/contribute/submissions'); -}; + useEffect(() => { + if (prevRes === resolution) return; + + startTransition(() => { + setSelectedContributions([]); + getContributionsOfFork(resolution).then(updateForkContributions); + }); + }, [resolution, prevRes, updateForkContributions]); + + useEffect(() => { + setSelectedContributions([]); + }, [activeTab]); + + const controls = Object.entries(Status).map(([key, value], index) => { + const isLast = index === Object.keys(Status).length - 1; + + return ( + <Button + key={key} + ref={setControlRef(index)} + onClick={() => setActiveTab(index)} + mod={{ active: activeTab === index }} + + pl="md" + pr="sm" + fullWidth + variant="filled" + leftSection={ + <> + <Indicator color={COLORS[value]} mr="md" /> + {value === Status.DRAFT + ? 'Drafted' + : value === Status.PENDING + ? 'Reviewed' + : value.charAt(0).toUpperCase() + value.slice(1).toLowerCase() + } + </> + } + rightSection={contributions.filter((c) => c.status === Object.keys(Status)[index]).length ?? 0} + justify="space-between" + className="slider-button" + style={windowWidth > BREAKPOINT_MOBILE_LARGE + ? { + borderRight: !isLast && activeTab !== index + 1 && activeTab !== index + ? 'calc(0.0625rem * var(--mantine-scale)) solid var(--mantine-color-default-border)' + : undefined, + borderRadius: isLast ? '0 calc(0.25rem * var(--mantine-scale)) calc(0.25rem * var(--mantine-scale)) 0' : undefined, + } + : undefined + } + /> + ); + }); + + const handleSubmitSelectedDraftContribution = () => { + startTransition(async () => { + await submitContributions(user.id!, selectedContributions); + await reload(); + }); + }; + + return ( + <Stack gap="xs"> + <Group + wrap="nowrap" + gap="xs" + > + <ActionIcon + variant="default" + className="navbar-icon-fix" + onClick={() => helpShown(!showHelp)} + > + <GoQuestion /> + </ActionIcon> + + <ActionIcon + variant="default" + className="navbar-icon-fix" + onClick={reload} + loading={loading} + > + <IoReload /> + </ActionIcon> + + <Select + w={120} + data={Object.keys(Resolution)} + checkIconPosition="right" + value={resolution} + onChange={(e) => e ? setResolution(e as Resolution) : null} + clearable={false} + /> + + <Button.Group + w="calc(100% - 34px - 34px - 120px - 180px - (3 * var(--mantine-spacing-xs)))" + ref={setGroupRef} + style={{ + position: 'relative', + }} + orientation={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'vertical' : 'horizontal'} + > + {controls} + <FloatingIndicator + target={controlsRefs[activeTab]} + parent={groupRef} + style={{ + border: 'calc(0.0625rem * var(--mantine-scale)) solid #fff3', + borderRadius: (() => { + if (activeTab === 0) return 'calc(0.25rem * var(--mantine-scale)) 0 0 calc(0.25rem * var(--mantine-scale))'; + if (activeTab === Object.keys(Status).length - 1) return '0 calc(0.25rem * var(--mantine-scale)) calc(0.25rem * var(--mantine-scale)) 0'; + return '0'; + })(), + backgroundColor: '#0002', + cursor: 'pointer', + zIndex: 200, + }} + /> + </Button.Group> + + {activeTab === 0 && ( + <Button + w={180} + variant="gradient" + gradient={GRADIENT} + disabled={selectedContributions.length === 0} + onClick={handleSubmitSelectedDraftContribution} + > + Submit {selectedContributions.length} draft{selectedContributions.length > 1 ? 's' : ''} + </Button> + )} + + {activeTab !== 0 && activeTab !== 4 && ( + <Button + w={180} + variant="gradient" + gradient={GRADIENT_DANGER} + disabled={selectedContributions.length === 0} + > + Unlist {selectedContributions.length} contribution{selectedContributions.length > 1 ? 's' : ''} + </Button> + )} + + {activeTab === 4 && ( + <Button + w={180} + variant="gradient" + gradient={GRADIENT_DANGER} + disabled={selectedContributions.length === 0} + > + Delete {selectedContributions.length} contribution{selectedContributions.length > 1 ? 's' : ''} + </Button> + )} + + </Group> + + {showHelp && ( + <Tile style={{ borderRadius: 'var(--mantine-radius-default)' }}> + <Text> + To contribute to the resource pack, you need to: + </Text> + <List ml="sm"> + <List.Item><Text fw={360}>Fork the default repository using the settings in your <Link href='/user/me'>account page</Link>;</Text></List.Item> + <List.Item><Text fw={360}>Clone it to your local machine using <Link href="https://git-scm.com/" target="_blank">Git</Link> or <Link href="https://desktop.github.com/download/" target="_blank">GitHub Desktop</Link>;</Text></List.Item> + <List.Item><Text fw={360}>Switch to the branch corresponding to the resolution you want to contribute to;</Text></List.Item> + <List.Item><Text fw={360}>Add textures to the repository, <Text component="span" fw={700}>each texture should have the same name as the contributed texture ID</Text>;</Text></List.Item> + <List.Item><Text fw={360}>Commit your changes and push them to your fork;</Text></List.Item> + <List.Item><Text fw={360}>Click the reload button to see your contributions here.</Text></List.Item> + </List> + + <Text mt="md"> + When reloading: + </Text> + <List ml="sm"> + <List.Item>New contributions will be added to the <Badge component="span" color={COLORS.DRAFT}>Drafted</Badge> tab;</List.Item> + <List.Item>Missing contributions that are either <Badge component="span" color={COLORS.PENDING}>Reviewed</Badge> or <Badge component="span" color={COLORS.REJECTED}>Rejected</Badge> will be deleted from the database;</List.Item> + <List.Item>Missing contributions that are <Badge component="span" color={COLORS.ACCEPTED}>Accepted</Badge> will be <Badge component="span" color={COLORS.ARCHIVED} style={{ color: 'black' }}>Archived</Badge>.</List.Item> + </List> + + <Text mt="md"> + A few tips: + </Text> + <List ml="sm"> + <List.Item>You have to click on contributions before submitting/unlisting/deleting them.</List.Item> + <List.Item>You can hit <Kbd component="span">{os === 'macos' ? '⌘' : 'Ctrl'}</Kbd> + <Kbd component="span">A</Kbd> to select them all</List.Item> + </List> + </Tile> + )} + + {hasFork === null && ( + <Group + align="center" + justify="center" + h="100px" + w="100%" + style={{ height: 'calc(81% - (2 * var(--mantine-spacing-sm) - 62px))' }} + > + <Text c="dimmed">You need to fork the default repository first</Text> + </Group> + )} + + {hasFork && ( + <Group gap="xs"> + {contributions.filter((c) => c.status === Object.keys(Status)[activeTab]).map((contribution, index) => { + const texture = textures.find((t) => t.id === contribution.textureId); + const orgOrUser = contribution.filepath.split('/')[3]!; + const repository = contribution.filepath.split('/')[4]!; + const commitSha = contribution.filepath.split('/')[5]!; + + return ( + ( + <TextureImage + key={index} + src={contribution.filepath} + alt={contribution.filename} + onClick={() => setSelectedContributions((prev) => { + if (prev.includes(contribution.id)) return prev.filter((id) => id !== contribution.id); + return [...prev, contribution.id]; + })} + styles={{ + boxShadow: selectedContributions.includes(contribution.id) ? '0 0 0 2px var(--mantine-color-teal-filled)' : 'none', + }} + popupStyles={{ + backgroundColor: 'transparent', + padding: 0, + border: 'none', + boxShadow: 'none', + }} + > + <Group gap={2}> + {texture && ( + <SmallTile color="gray" w={125} h="100%"> + <Group w="100%" h="100%" justify="center" align="center"> + <TextureImage + src={texture.filepath} + alt={texture.name} + mcmeta={texture.mcmeta} + size={115} + /> + </Group> + </SmallTile> + )} + <Stack gap={2} align="start" miw={400} maw={400}> + <SmallTile color="gray"> + <Text fw={500} ta="center">{texture?.name}</Text> + </SmallTile> + + <Group gap={2} w="100%" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <GoCommit /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs"> + <a + href={gitCommitUrl({ orgOrUser, repository, commitSha })} + target="_blank" + rel="noreferrer" + > + {commitSha} + </a> + </Text> + </SmallTile> + </Group> + + <Group gap={2} w="100%" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <GoRelFilePath /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs"> + <a + href={gitBlobUrl({ orgOrUser, repository, branchOrCommit: commitSha, path: contribution.filename })} + target="_blank" + rel="noreferrer" + > + {contribution.filename} + </a> + </Text> + </SmallTile> + </Group> + + <Group gap={2} w="100%"> + <Group gap={2} w="calc((100% - 4px) / 3)" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <LuArrowUpDown /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs"> + {contribution.poll.upvotes.length - contribution.poll.downvotes.length} + </Text> + </SmallTile> + </Group> + <Group gap={2} w="calc((100% - 4px) / 3)" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <GoHash /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs"> + {contribution.textureId} + </Text> + </SmallTile> + </Group> + <Group gap={2} w="calc((100% - 4px) / 3)" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <GoHourglass /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs"> + {contribution.status} + </Text> + </SmallTile> + </Group> + </Group> + </Stack> + </Group> + </TextureImage> + ) + ); + })} + </Group> + )} + + </Stack> + ); +} -export default CouncilPage; diff --git a/src/app/(pages)/(protected)/contribute/submissions/styles.scss b/src/app/(pages)/(protected)/contribute/styles.scss similarity index 100% rename from src/app/(pages)/(protected)/contribute/submissions/styles.scss rename to src/app/(pages)/(protected)/contribute/styles.scss diff --git a/src/app/(pages)/(protected)/contribute/submissions/page.tsx b/src/app/(pages)/(protected)/contribute/submissions/page.tsx deleted file mode 100644 index cc6f1edc..00000000 --- a/src/app/(pages)/(protected)/contribute/submissions/page.tsx +++ /dev/null @@ -1,312 +0,0 @@ -'use client'; - -import { useCallback, useEffect, useState, useTransition } from 'react'; - -import { GoCommit, GoHash, GoHourglass, GoRelFilePath } from 'react-icons/go'; -import { IoReload } from 'react-icons/io5'; -import { LuArrowUpDown } from 'react-icons/lu'; - -import { ActionIcon, Button, FloatingIndicator, Group, Indicator, Select, Stack, Text } from '@mantine/core'; -import { usePrevious } from '@mantine/hooks'; -import { Resolution, Status } from '@prisma/client'; - -import { SmallTile } from '~/components/small-tile'; -import { TextureImage } from '~/components/texture-img'; -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, COLORS, gitBlobUrl, gitCommitUrl } from '~/lib/constants'; -import { getContributionsOfFork, getFork } from '~/server/actions/git'; -import { createContributionsFromGitFiles, deleteContributionsOrArchive, getContributionsOfUser } from '~/server/data/contributions'; -import { getTextures } from '~/server/data/texture'; - -import type { Texture } from '@prisma/client'; -import type { GitFile } from '~/server/actions/git'; -import type { GetContributionsOfUser } from '~/server/data/contributions'; - -import './styles.scss'; - -export default function ContributeSubmissionsPage() { - const user = useCurrentUser()!; // the user is guaranteed to be logged in (per the layout) - - const [loading, startTransition] = useTransition(); - const [hasFork, setHasFork] = useState<string | null>(null); - - const [resolution, setResolution] = useState<Resolution>(Resolution.x32); - const prevRes = usePrevious(resolution); - - const [contributions, setContributions] = useState<GetContributionsOfUser[]>([]); - const [textures, setTextures] = useState<Texture[]>([]); - - const [groupRef, setGroupRef] = useState<HTMLDivElement | null>(null); - const [controlsRefs, setControlsRefs] = useState<Record<string, HTMLButtonElement | null>>({}); - const [activeTab, setActiveTab] = useState(0); - const [windowWidth] = useDeviceSize(); - - const setControlRef = (index: number) => (node: HTMLButtonElement) => { - controlsRefs[index] = node; - setControlsRefs(controlsRefs); - }; - - const reload = async () => { - startTransition(async () => { - const fork = await getFork(); - if (!fork) return; - - setHasFork(fork); - getContributionsOfFork(resolution).then(updateForkContributions); - }); - }; - - const updateForkContributions = useCallback(async (files: GitFile[]) => { - const contributions = await getContributionsOfUser(user.id!, resolution); - const contributedSha = contributions.map((contribution) => contribution.hash); - - // delete contributions that are not in the fork but are in the database - const missingFiles = contributions.filter((contribution) => !files.some((file) => file.sha === contribution.hash)); - await deleteContributionsOrArchive(user.id!, missingFiles.map((contribution) => contribution.id)); - - // add contributions that are not yet in the database - const newFiles = files.filter((file) => !contributedSha.includes(file.sha)); - await createContributionsFromGitFiles(user.id!, resolution, newFiles); - - const contributionsAfter = await getContributionsOfUser(user.id!, resolution); - setContributions(contributionsAfter); - - }, [user, resolution]); - - useEffectOnce(() => { - reload(); - getTextures().then(setTextures); - }); - - useEffect(() => { - if (prevRes === resolution) return; - - startTransition(() => { - getContributionsOfFork(resolution).then(updateForkContributions); - }); - }, [resolution, prevRes, updateForkContributions]); - - const controls = Object.entries(Status).map(([key, value], index) => { - const isLast = index === Object.keys(Status).length - 1; - - return ( - <Button - key={key} - ref={setControlRef(index)} - onClick={() => setActiveTab(index)} - mod={{ active: activeTab === index }} - - pl="md" - pr="sm" - fullWidth - variant="filled" - leftSection={ - <> - <Indicator color={COLORS[value]} mr="md" /> - {value === Status.DRAFT - ? 'Drafted' - : value === Status.PENDING - ? 'Reviewed' - : value.charAt(0).toUpperCase() + value.slice(1).toLowerCase() - } - </> - } - rightSection={0} - justify="space-between" - className="slider-button" - style={windowWidth > BREAKPOINT_MOBILE_LARGE - ? { - borderRight: !isLast && activeTab !== index + 1 && activeTab !== index - ? 'calc(0.0625rem * var(--mantine-scale)) solid var(--mantine-color-default-border)' - : undefined, - borderRadius: isLast ? '0 calc(0.25rem * var(--mantine-scale)) calc(0.25rem * var(--mantine-scale)) 0' : undefined, - } - : undefined - } - /> - ); - }); - - return ( - <Stack gap="xs"> - <Group - wrap="nowrap" - gap="xs" - > - <ActionIcon - variant="default" - className="navbar-icon-fix" - onClick={reload} - loading={loading} - > - <IoReload /> - </ActionIcon> - <Select - w={120} - data={Object.keys(Resolution)} - checkIconPosition="right" - value={resolution} - onChange={(e) => e ? setResolution(e as Resolution) : null} - clearable={false} - /> - <Button.Group - w="calc(100% - 120px - var(--mantine-spacing-xs))" - ref={setGroupRef} - style={{ - position: 'relative', - }} - orientation={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'vertical' : 'horizontal'} - > - {controls} - - <FloatingIndicator - target={controlsRefs[activeTab]} - parent={groupRef} - style={{ - border: 'calc(0.0625rem * var(--mantine-scale)) solid #fff3', - borderRadius: (() => { - if (activeTab === 0) return 'calc(0.25rem * var(--mantine-scale)) 0 0 calc(0.25rem * var(--mantine-scale))'; - if (activeTab === Object.keys(Status).length - 1) return '0 calc(0.25rem * var(--mantine-scale)) calc(0.25rem * var(--mantine-scale)) 0'; - return '0'; - })(), - backgroundColor: '#0002', - cursor: 'pointer', - zIndex: 200, - }} - /> - - </Button.Group> - </Group> - - {hasFork === null && ( - <Group - align="center" - justify="center" - h="100px" - w="100%" - style={{ height: 'calc(81% - (2 * var(--mantine-spacing-sm) - 62px))' }} - > - <Text c="dimmed">You need to fork the default repository first</Text> - </Group> - )} - - {hasFork && ( - <Group gap="xs"> - {contributions.map((contribution) => { - const texture = textures.find((t) => t.id === contribution.textureId); - const orgOrUser = contribution.filepath.split('/')[3]!; - const repository = contribution.filepath.split('/')[4]!; - const commitSha = contribution.filepath.split('/')[5]!; - - return ( - ( - <TextureImage - key={contribution.id} - src={contribution.filepath} - alt={contribution.filename} - popupStyles={{ - backgroundColor: 'transparent', - padding: 0, - border: 'none', - boxShadow: 'none', - }} - > - <Group gap={2}> - {texture && ( - <SmallTile color="gray" w={125} h="100%"> - <Group w="100%" h="100%" justify="center" align="center"> - <TextureImage - src={texture.filepath} - alt={texture.name} - mcmeta={texture.mcmeta} - size={115} - /> - </Group> - </SmallTile> - )} - <Stack gap={2} align="start" miw={400} maw={400}> - <SmallTile color="gray"> - <Text fw={500} ta="center">{texture?.name}</Text> - </SmallTile> - - <Group gap={2} w="100%" wrap="nowrap" align="start"> - <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> - <GoCommit /> - </SmallTile> - <SmallTile color="gray"> - <Text size="xs"> - <a - href={gitCommitUrl({ orgOrUser, repository, commitSha })} - target="_blank" - rel="noreferrer" - > - {commitSha} - </a> - </Text> - </SmallTile> - </Group> - - <Group gap={2} w="100%" wrap="nowrap" align="start"> - <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> - <GoRelFilePath /> - </SmallTile> - <SmallTile color="gray"> - <Text size="xs"> - <a - href={gitBlobUrl({ orgOrUser, repository, branchOrCommit: commitSha, path: contribution.filename })} - target="_blank" - rel="noreferrer" - > - {contribution.filename} - </a> - </Text> - </SmallTile> - </Group> - - <Group gap={2} w="100%"> - <Group gap={2} w="calc((100% - 4px) / 3)" wrap="nowrap" align="start"> - <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> - <LuArrowUpDown /> - </SmallTile> - <SmallTile color="gray"> - <Text size="xs"> - {contribution.poll.upvotes.length - contribution.poll.downvotes.length} - </Text> - </SmallTile> - </Group> - <Group gap={2} w="calc((100% - 4px) / 3)" wrap="nowrap" align="start"> - <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> - <GoHash /> - </SmallTile> - <SmallTile color="gray"> - <Text size="xs"> - {contribution.textureId} - </Text> - </SmallTile> - </Group> - <Group gap={2} w="calc((100% - 4px) / 3)" wrap="nowrap" align="start"> - <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> - <GoHourglass /> - </SmallTile> - <SmallTile color="gray"> - <Text size="xs"> - {contribution.status} - </Text> - </SmallTile> - </Group> - </Group> - </Stack> - </Group> - </TextureImage> - ) - ); - })} - </Group> - )} - - </Stack> - ); -} - diff --git a/src/server/data/contributions.ts b/src/server/data/contributions.ts index cae82b66..a26d5f46 100644 --- a/src/server/data/contributions.ts +++ b/src/server/data/contributions.ts @@ -117,6 +117,15 @@ export async function getLatestContributionsOfModVersion(modVersionId: string, r // POST +export async function submitContributions(ownerId: string, contributionsIds: string[]) { + await canAccess(UserRole.USER, ownerId); + + await db.contribution.updateMany({ + where: { id: { in: contributionsIds }, ownerId }, + data: { status: Status.PENDING }, + }); +} + export async function checkContributionStatus(contributionId: string) { await canAccess(UserRole.COUNCIL); From ae623b005ab3ff48690a742d3963493d4bfe865a Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Sun, 18 Aug 2024 03:58:00 +0200 Subject: [PATCH 08/30] fix : fix mods resource pack download for the new contributions --- src/app/api/download/mods/[modVerId]/[res]/route.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/app/api/download/mods/[modVerId]/[res]/route.ts b/src/app/api/download/mods/[modVerId]/[res]/route.ts index dce3a271..fea7745a 100644 --- a/src/app/api/download/mods/[modVerId]/[res]/route.ts +++ b/src/app/api/download/mods/[modVerId]/[res]/route.ts @@ -3,7 +3,7 @@ import { createReadStream } from 'fs'; import { Resolution, Status } from '@prisma/client'; import JSZip from 'jszip'; -import { FILE_DIR, FILE_PATH, PUBLIC_PATH } from '~/lib/constants'; +import { PUBLIC_PATH } from '~/lib/constants'; import { db } from '~/lib/db'; import { getPackFormatVersion, getVanillaTextureSrc, sortBySemver } from '~/lib/utils'; import { getModVersionProgression } from '~/server/data/mods-version'; @@ -79,10 +79,8 @@ export async function GET(req: Request, { params: { modVerId, res } }: Params) { const contribution = linkedTexture.texture.contributions[0]!; if (contribution) { - zip.file<'stream'>( - `${linkedTexture.assetPath}`, - createReadStream(`${FILE_PATH}/${contribution.filepath.replace('/files', '/').replace(FILE_DIR, '')}`) - ); + const contributionTexture = await fetch(contribution.filepath); + zip.file<'arraybuffer'>(`${linkedTexture.assetPath}`, contributionTexture.arrayBuffer()); if (contribution.mcmeta) { zip.file<'text'>( @@ -100,8 +98,8 @@ export async function GET(req: Request, { params: { modVerId, res } }: Params) { const vanillaTextureId = linkedTexture.texture.vanillaTextureId; if (vanillaTextureId) { - const vanillaTexture = (await fetch(getVanillaTextureSrc(vanillaTextureId, res))).arrayBuffer(); - zip.file<'arraybuffer'>(`${linkedTexture.assetPath}`, vanillaTexture); + const vanillaTexture = await fetch(getVanillaTextureSrc(vanillaTextureId, res)); + zip.file<'arraybuffer'>(`${linkedTexture.assetPath}`, vanillaTexture.arrayBuffer()); if (linkedTexture.texture.mcmeta) { zip.file<'text'>( From 2b149f94510bb75f915e19bf8c6cd9fc3ca98572 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Sun, 18 Aug 2024 03:58:20 +0200 Subject: [PATCH 09/30] fix : delete contributions download endpoint #134 --- .../download/contributions/[ownerId]/route.ts | 43 ------------------- 1 file changed, 43 deletions(-) delete mode 100644 src/app/api/download/contributions/[ownerId]/route.ts diff --git a/src/app/api/download/contributions/[ownerId]/route.ts b/src/app/api/download/contributions/[ownerId]/route.ts deleted file mode 100644 index 64f3a70f..00000000 --- a/src/app/api/download/contributions/[ownerId]/route.ts +++ /dev/null @@ -1,43 +0,0 @@ - -import { createReadStream } from 'fs'; - -import { UserRole } from '@prisma/client'; -import JSZip from 'jszip'; - -import { canAccess } from '~/lib/auth'; -import { FILE_DIR, FILE_PATH } from '~/lib/constants'; -import { db } from '~/lib/db'; - -interface Params { - params: { - ownerId: string; - }; -} - -export async function GET(req: Request, { params: { ownerId } }: Params) { - await canAccess(UserRole.ADMIN, ownerId); - - const contributions = await db.contribution.findMany({ - where: { ownerId }, - select: { filepath: true, filename: true, status: true, hash: true }, - }); - - const zip = new JSZip(); - - for (const contribution of contributions) { - zip.file<'stream'>( - `${contribution.status}/${contribution.hash}_${contribution.filename}`, - createReadStream(`${FILE_PATH}/${contribution.filepath.replace('/files', '/').replace(FILE_DIR, '')}`) - ); - } - - const zipFile = await zip.generateAsync({ type: 'nodebuffer' }); - - return new Response(zipFile, { - status: 200, - headers: { - 'Content-Type': 'application/zip', - }, - }); - -} From 5264e0576f365783a9f016314eb1113dbd46b5d6 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Sun, 18 Aug 2024 03:59:26 +0200 Subject: [PATCH 10/30] fix : remove unused function --- src/server/actions/git.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/server/actions/git.ts b/src/server/actions/git.ts index 95ea9619..359a2d37 100644 --- a/src/server/actions/git.ts +++ b/src/server/actions/git.ts @@ -293,17 +293,6 @@ async function createCommit({ message, treeSha, parentCommitSha, owner, repo }: return data; } -async function createEmptyTree(owner = GITHUB_ORG_NAME, repo = GITHUB_DEFAULT_REPO_NAME) { - const octokit = await getOctokit(); - const { data } = await octokit.git.createTree({ - owner, - repo, - tree: [], - }); - - return data.sha; -} - interface CreateNewTreeOptions { filenames: string[], blobs: { url: string, sha: string }[], From cb00138c36bfd33e722fd187a52f17dc91e04ced Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Sun, 18 Aug 2024 04:00:29 +0200 Subject: [PATCH 11/30] fix : hide help by default --- src/app/(pages)/(protected)/contribute/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(pages)/(protected)/contribute/page.tsx b/src/app/(pages)/(protected)/contribute/page.tsx index 82cd8a33..12fae3b6 100644 --- a/src/app/(pages)/(protected)/contribute/page.tsx +++ b/src/app/(pages)/(protected)/contribute/page.tsx @@ -35,7 +35,7 @@ export default function ContributeSubmissionsPage() { const [loading, startTransition] = useTransition(); const [hasFork, setHasFork] = useState<string | null>(null); - const [showHelp, helpShown] = useState(true); + const [showHelp, helpShown] = useState(false); const [selectedContributions, setSelectedContributions] = useState<string[]>([]); From 648a28a43d7cda138628d1309b7879f9f0b14567 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Sun, 18 Aug 2024 04:03:08 +0200 Subject: [PATCH 12/30] fix : remove sync button (not needed) --- .../(protected)/user/[userId]/page.tsx | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/app/(pages)/(protected)/user/[userId]/page.tsx b/src/app/(pages)/(protected)/user/[userId]/page.tsx index 59e04f79..30d95cba 100644 --- a/src/app/(pages)/(protected)/user/[userId]/page.tsx +++ b/src/app/(pages)/(protected)/user/[userId]/page.tsx @@ -107,26 +107,16 @@ const UserPage = () => { if (hasFork) { return ( <Tile p="xs" pl={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'xs' : 'md'} color="teal"> - <Group justify="space-between" gap="xs"> - <Group gap="sm"> - <GoCheckCircle size={20} color="white" /> - <Group gap={3}> - <Text size="sm" c="white">Default textures repository forked: </Text> - <Text size="sm" c="white"> - <Link href={hasFork} style={{ color: 'white' }}> - {windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'link' : hasFork} - </Link> - </Text> - </Group> + <Group gap="sm"> + <GoCheckCircle size={20} color="white" /> + <Group gap={3}> + <Text size="sm" c="white">Default textures repository forked: </Text> + <Text size="sm" c="white"> + <Link href={hasFork} style={{ color: 'white' }}> + {windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'link' : hasFork} + </Link> + </Text> </Group> - - <Button - variant="outline" - color="white" - fullWidth={windowWidth <= BREAKPOINT_MOBILE_LARGE} - > - Sync Fork - </Button> </Group> </Tile> ); From d069c767d296781228d07f0079a3a050fa52bb74 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Sun, 18 Aug 2024 04:10:26 +0200 Subject: [PATCH 13/30] feat : (part 4) add missing functions --- .../(pages)/(protected)/contribute/page.tsx | 33 ++++++++++++------- src/server/data/contributions.ts | 25 +++++++++++++- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/app/(pages)/(protected)/contribute/page.tsx b/src/app/(pages)/(protected)/contribute/page.tsx index 12fae3b6..efc85f08 100644 --- a/src/app/(pages)/(protected)/contribute/page.tsx +++ b/src/app/(pages)/(protected)/contribute/page.tsx @@ -20,7 +20,7 @@ import { useDeviceSize } from '~/hooks/use-device-size'; import { useEffectOnce } from '~/hooks/use-effect-once'; import { BREAKPOINT_MOBILE_LARGE, COLORS, gitBlobUrl, gitCommitUrl, GRADIENT, GRADIENT_DANGER } from '~/lib/constants'; import { getContributionsOfFork, getFork } from '~/server/actions/git'; -import { createContributionsFromGitFiles, deleteContributionsOrArchive, getContributionsOfUser, submitContributions } from '~/server/data/contributions'; +import { archiveContributions, createContributionsFromGitFiles, deleteContributions, deleteContributionsOrArchive, getContributionsOfUser, submitContributions } from '~/server/data/contributions'; import { getTextures } from '~/server/data/texture'; import type { Texture } from '@prisma/client'; @@ -145,13 +145,6 @@ export default function ContributeSubmissionsPage() { ); }); - const handleSubmitSelectedDraftContribution = () => { - startTransition(async () => { - await submitContributions(user.id!, selectedContributions); - await reload(); - }); - }; - return ( <Stack gap="xs"> <Group @@ -216,7 +209,12 @@ export default function ContributeSubmissionsPage() { variant="gradient" gradient={GRADIENT} disabled={selectedContributions.length === 0} - onClick={handleSubmitSelectedDraftContribution} + onClick={() => { + startTransition(async () => { + await submitContributions(user.id!, selectedContributions); + await reload(); + }); + }} > Submit {selectedContributions.length} draft{selectedContributions.length > 1 ? 's' : ''} </Button> @@ -228,8 +226,14 @@ export default function ContributeSubmissionsPage() { variant="gradient" gradient={GRADIENT_DANGER} disabled={selectedContributions.length === 0} + onClick={() => { + startTransition(async () => { + await archiveContributions(user.id!, selectedContributions); + await reload(); + }); + }} > - Unlist {selectedContributions.length} contribution{selectedContributions.length > 1 ? 's' : ''} + Archive {selectedContributions.length} contribution{selectedContributions.length > 1 ? 's' : ''} </Button> )} @@ -239,6 +243,12 @@ export default function ContributeSubmissionsPage() { variant="gradient" gradient={GRADIENT_DANGER} disabled={selectedContributions.length === 0} + onClick={() => { + startTransition(async () => { + await deleteContributions(user.id!, selectedContributions); + await reload(); + }); + }} > Delete {selectedContributions.length} contribution{selectedContributions.length > 1 ? 's' : ''} </Button> @@ -273,8 +283,9 @@ export default function ContributeSubmissionsPage() { A few tips: </Text> <List ml="sm"> - <List.Item>You have to click on contributions before submitting/unlisting/deleting them.</List.Item> + <List.Item>You have to click on contributions before submitting/archiving/deleting them.</List.Item> <List.Item>You can hit <Kbd component="span">{os === 'macos' ? '⌘' : 'Ctrl'}</Kbd> + <Kbd component="span">A</Kbd> to select them all</List.Item> + <List.Item>If you delete an archived contribution, make sure to delete it from your fork first, otherwise it will be re-added to the database as a draft.</List.Item> </List> </Tile> )} diff --git a/src/server/data/contributions.ts b/src/server/data/contributions.ts index a26d5f46..9bedec3e 100644 --- a/src/server/data/contributions.ts +++ b/src/server/data/contributions.ts @@ -118,7 +118,7 @@ export async function getLatestContributionsOfModVersion(modVersionId: string, r // POST export async function submitContributions(ownerId: string, contributionsIds: string[]) { - await canAccess(UserRole.USER, ownerId); + await canAccess(UserRole.ADMIN, ownerId); await db.contribution.updateMany({ where: { id: { in: contributionsIds }, ownerId }, @@ -126,6 +126,15 @@ export async function submitContributions(ownerId: string, contributionsIds: str }); } +export async function archiveContributions(ownerId: string, contributionsIds: string[]) { + await canAccess(UserRole.ADMIN, ownerId); + + await db.contribution.updateMany({ + where: { id: { in: contributionsIds }, ownerId }, + data: { status: Status.ARCHIVED }, + }); +} + export async function checkContributionStatus(contributionId: string) { await canAccess(UserRole.COUNCIL); @@ -228,3 +237,17 @@ export async function deleteContributionsOrArchive( } } } + +export async function deleteContributions(ownerId: string, ids: string[]) { + await canAccess(UserRole.ADMIN, ownerId); + + const contributions = await db.contribution.findMany({ + where: { id: { in: ids } }, + include: { coAuthors: { select: { id: true } } }, + }); + + for (const contribution of contributions) { + await db.contribution.delete({ where: { id: contribution.id } }); + await db.poll.delete({ where: { id: contribution.pollId } }); + } +} From 65eafac58c96e3e163df21eb30c9432657a97ca1 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Sun, 18 Aug 2024 04:16:22 +0200 Subject: [PATCH 14/30] fix : add messages --- .../(pages)/(protected)/contribute/page.tsx | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/app/(pages)/(protected)/contribute/page.tsx b/src/app/(pages)/(protected)/contribute/page.tsx index efc85f08..100e13c5 100644 --- a/src/app/(pages)/(protected)/contribute/page.tsx +++ b/src/app/(pages)/(protected)/contribute/page.tsx @@ -302,6 +302,30 @@ export default function ContributeSubmissionsPage() { </Group> )} + {contributions.length !== 0 && contributions.filter((c) => c.status === Object.keys(Status)[activeTab]).length === 0 && ( + <Group + align="center" + justify="center" + h="100px" + w="100%" + style={{ height: 'calc(81% - (2 * var(--mantine-spacing-sm) - 62px))' }} + > + <Text c="dimmed">No contributions to show</Text> + </Group> + )} + + {contributions.length === 0 && ( + <Group + align="center" + justify="center" + h="100px" + w="100%" + style={{ height: 'calc(81% - (2 * var(--mantine-spacing-sm) - 62px))' }} + > + <Text c="dimmed">No contributions, you need to push some textures to your fork first!</Text> + </Group> + )} + {hasFork && ( <Group gap="xs"> {contributions.filter((c) => c.status === Object.keys(Status)[activeTab]).map((contribution, index) => { @@ -422,7 +446,6 @@ export default function ContributeSubmissionsPage() { })} </Group> )} - </Stack> ); } From f4f76fba2f01b20a4b1ca8796d8b0c2afab6dcc6 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Sun, 18 Aug 2024 04:43:46 +0200 Subject: [PATCH 15/30] fix : use a different repo for dev --- src/lib/constants.ts | 6 ++---- src/server/actions/git.ts | 20 +++++--------------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 04e9d4c5..69af153b 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -106,7 +106,7 @@ export const COLORS: Record<Status, MantineColor> = { }; export const GITHUB_ORG_NAME = 'faithful-mods'; -export const GITHUB_DEFAULT_REPO_NAME = 'resources-default'; +export const GITHUB_DEFAULT_REPO_NAME = process.env.NODE_ENV === 'production' ? 'resources-default' : 'resources-default-dev'; export type RawUrl = | `https://raw.githubusercontent.com/${string}/${string}/${string}` @@ -121,9 +121,7 @@ export type FileGitParams = { export const gitRawUrl = ({ orgOrUser, repository, branchOrCommit, path }: FileGitParams): RawUrl => { if (branchOrCommit) return `https://raw.githubusercontent.com/${orgOrUser}/${repository}/${branchOrCommit}/${path}`; - const branch = process.env.NODE_ENV === 'production' ? 'main' : 'dev'; - - return `https://raw.githubusercontent.com/${orgOrUser}/${repository}/${branch}/${path}`; + return `https://raw.githubusercontent.com/${orgOrUser}/${repository}/main/${path}`; }; export type CommitUrl = `https://github.com/${string}/${string}/commit/${string}`; diff --git a/src/server/actions/git.ts b/src/server/actions/git.ts index 359a2d37..6051f58b 100644 --- a/src/server/actions/git.ts +++ b/src/server/actions/git.ts @@ -107,16 +107,9 @@ export async function forkRepository() { await createBranchFromCommit(username, GITHUB_DEFAULT_REPO_NAME, resolution, firstCommitSha); } - // rename the dev/main branch to x16 (if prod => main => x16 and DEL dev, if dev => dev => x16 and DEL main) - if (process.env.NODE_ENV !== 'production') { - await setDefaultBranch(username, GITHUB_DEFAULT_REPO_NAME, 'dev'); - await deleteBranch(username, GITHUB_DEFAULT_REPO_NAME, 'main'); - await renameBranch(username, GITHUB_DEFAULT_REPO_NAME, 'dev', 'x16'); - } - else { - await deleteBranch(username, GITHUB_DEFAULT_REPO_NAME, 'dev'); - await renameBranch(username, GITHUB_DEFAULT_REPO_NAME, 'main', 'x16'); - } + // set x32 as default branch and delete main (x16) as it's not needed + await setDefaultBranch(username, GITHUB_DEFAULT_REPO_NAME, 'x32'); + await deleteBranch(username, GITHUB_DEFAULT_REPO_NAME, 'main'); } /** @@ -128,10 +121,8 @@ export async function forkRepository() { export async function uploadToRepository(files: base64[], filenames: string[], commitMessage: string): Promise<void> { await canAccess(UserRole.COUNCIL); - const branch = process.env.NODE_ENV === 'production' ? 'main' : 'dev'; - // get latest commit - const currentCommit = await getCurrentCommit(branch); + const currentCommit = await getCurrentCommit('main'); // create blobs for each file const blobs = await Promise.all(files.map((file) => createBlobFile(file))); @@ -143,7 +134,7 @@ export async function uploadToRepository(files: base64[], filenames: string[], c const newCommit = await createCommit({ message: commitMessage, treeSha: newTree.sha, parentCommitSha: currentCommit.commit_sha }); // update the branch to point to the new commit - await setBranchToCommit(newCommit.sha, branch); + await setBranchToCommit(newCommit.sha, 'main'); } export interface GitFile { @@ -158,7 +149,6 @@ export interface GitFile { export async function getContributionsOfFork(resolution: Resolution): Promise<GitFile[]> { const username = await getUserGitHubUsername(); const files = await listFilesInBranch(username, GITHUB_DEFAULT_REPO_NAME, resolution); - console.log(files, resolution); return files as GitFile[]; } From 655be2175bef8296427046c82d2dfdb9759e92c1 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Sun, 18 Aug 2024 04:44:14 +0200 Subject: [PATCH 16/30] fix : use hashes instead of texture ids as it's immutable --- src/server/actions/files.ts | 4 ++-- src/server/data/contributions.ts | 16 +++++----------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/server/actions/files.ts b/src/server/actions/files.ts index 8eca11b8..7d78eb15 100644 --- a/src/server/actions/files.ts +++ b/src/server/actions/files.ts @@ -345,11 +345,11 @@ export async function extractDefaultResourcePack(jar: File, modVersion: ModVersi mcmeta, }); - const filepath = `${gitRawUrl({ orgOrUser: GITHUB_ORG_NAME, repository: GITHUB_DEFAULT_REPO_NAME })}/${texture.id}.png` as const; + const filepath = gitRawUrl({ orgOrUser: GITHUB_ORG_NAME, repository: GITHUB_DEFAULT_REPO_NAME, path: `${hash}.png` }); await db.texture.update({ where: { id: texture.id }, data: { filepath } }); filesToCommit.push(buffer.toString('base64') as base64); - filesNamesOnGit.push(`${texture.id}.png`); + filesNamesOnGit.push(`${hash}.png`); } else { if (texture.name !== textureName && !texture.aliases.includes(textureName)) { diff --git a/src/server/data/contributions.ts b/src/server/data/contributions.ts index 9bedec3e..a4944528 100644 --- a/src/server/data/contributions.ts +++ b/src/server/data/contributions.ts @@ -174,16 +174,10 @@ export async function createContributionsFromGitFiles(ownerId: string, resolutio const existingContribution = await db.contribution.findFirst({ where: { hash: file.sha } }); if (existingContribution) continue; - let textureId: number; - - try { - const filename = file.path.includes('/') ? file.path.split('/')[1] : file.path; - textureId = parseInt(filename?.replace('.png', '') ?? '', 10); - if (isNaN(textureId)) continue; - } catch (e) { - console.error(e); - continue; - } + const hash = (file.path.includes('/') ? file.path.split('/')[1] : file.path)?.replace('.png', ''); + const texture = await db.texture.findFirst({ where: { hash } }); + + if (!texture) continue; const poll = await db.poll.create({ data: {} }); const contribution = await db.contribution.create({ @@ -195,7 +189,7 @@ export async function createContributionsFromGitFiles(ownerId: string, resolutio pollId: poll.id, filename: file.path, resolution, - textureId, + textureId: texture.id, }, }); From af9a9e6b24269864755db95601e136a29b89ba94 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Sun, 18 Aug 2024 04:44:25 +0200 Subject: [PATCH 17/30] fix : improve wording --- src/app/(pages)/(protected)/contribute/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/(pages)/(protected)/contribute/page.tsx b/src/app/(pages)/(protected)/contribute/page.tsx index 100e13c5..4893ce40 100644 --- a/src/app/(pages)/(protected)/contribute/page.tsx +++ b/src/app/(pages)/(protected)/contribute/page.tsx @@ -259,13 +259,13 @@ export default function ContributeSubmissionsPage() { {showHelp && ( <Tile style={{ borderRadius: 'var(--mantine-radius-default)' }}> <Text> - To contribute to the resource pack, you need to: + To contribute, you need to: </Text> <List ml="sm"> - <List.Item><Text fw={360}>Fork the default repository using the settings in your <Link href='/user/me'>account page</Link>;</Text></List.Item> + <List.Item><Text fw={360}>Fork the default repository using the specified area in your <Link href='/user/me'>user page</Link>;</Text></List.Item> <List.Item><Text fw={360}>Clone it to your local machine using <Link href="https://git-scm.com/" target="_blank">Git</Link> or <Link href="https://desktop.github.com/download/" target="_blank">GitHub Desktop</Link>;</Text></List.Item> <List.Item><Text fw={360}>Switch to the branch corresponding to the resolution you want to contribute to;</Text></List.Item> - <List.Item><Text fw={360}>Add textures to the repository, <Text component="span" fw={700}>each texture should have the same name as the contributed texture ID</Text>;</Text></List.Item> + <List.Item><Text fw={360}>Add textures to the repository, <Text component="span" fw={700}>each texture should have the same name as the contributed texture name in the <Link href="https://github.com/faithful-mods/resources-default" target="_blank">default repository</Link></Text>;</Text></List.Item> <List.Item><Text fw={360}>Commit your changes and push them to your fork;</Text></List.Item> <List.Item><Text fw={360}>Click the reload button to see your contributions here.</Text></List.Item> </List> From b3dbf51dda85dbb1027998b7caf576605afffae6 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Mon, 19 Aug 2024 00:35:13 +0200 Subject: [PATCH 18/30] fix : adapt user page to small devices --- src/app/(pages)/(protected)/user/[userId]/page.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/(pages)/(protected)/user/[userId]/page.tsx b/src/app/(pages)/(protected)/user/[userId]/page.tsx index 30d95cba..1f18dd7e 100644 --- a/src/app/(pages)/(protected)/user/[userId]/page.tsx +++ b/src/app/(pages)/(protected)/user/[userId]/page.tsx @@ -106,8 +106,8 @@ const UserPage = () => { const forkedInfo = () => { if (hasFork) { return ( - <Tile p="xs" pl={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'xs' : 'md'} color="teal"> - <Group gap="sm"> + <Tile p="xs" pl={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'xs' : 'md'} color="teal" mih={56}> + <Group gap="sm" mt="auto" mb="auto"> <GoCheckCircle size={20} color="white" /> <Group gap={3}> <Text size="sm" c="white">Default textures repository forked: </Text> @@ -208,6 +208,7 @@ const UserPage = () => { > <Button justify={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'center' : 'right'} + fullWidth={windowWidth <= BREAKPOINT_MOBILE_LARGE} variant="transparent" color="red" onClick={() => signOut({ callbackUrl: '/' })} @@ -219,6 +220,7 @@ const UserPage = () => { gradient={GRADIENT} onClick={() => onSubmit(form.values)} disabled={loading || !form.isValid() || user === undefined} + fullWidth={windowWidth <= BREAKPOINT_MOBILE_LARGE} loading={loading} > Save @@ -244,7 +246,7 @@ const UserPage = () => { borderColor: 'var(--mantine-color-red-filled)', }} > - <Group justify="space-between"> + <Group justify="space-between" style={{ opacity: hasFork ? 1 : .5 }}> <Stack gap={0}> <Text>Delete the forked repository</Text> <Text c="dimmed" size="xs">This action is irreversible, all contributions will be lost.</Text> From 3a4b8cedbdfc7d9056f345628a7414bfd436ee92 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Mon, 19 Aug 2024 00:57:34 +0200 Subject: [PATCH 19/30] feat : add the fork button to the contribute page --- .../(pages)/(protected)/contribute/page.tsx | 31 +++---- .../(protected)/user/[userId]/page.tsx | 69 ++------------- src/components/fork.tsx | 83 +++++++++++++++++++ 3 files changed, 102 insertions(+), 81 deletions(-) create mode 100644 src/components/fork.tsx diff --git a/src/app/(pages)/(protected)/contribute/page.tsx b/src/app/(pages)/(protected)/contribute/page.tsx index 4893ce40..1bf62447 100644 --- a/src/app/(pages)/(protected)/contribute/page.tsx +++ b/src/app/(pages)/(protected)/contribute/page.tsx @@ -12,6 +12,7 @@ import { ActionIcon, Badge, Button, FloatingIndicator, Group, Indicator, Kbd, Li import { useHotkeys, useOs, usePrevious } from '@mantine/hooks'; import { Resolution, Status } from '@prisma/client'; +import ForkInfo from '~/components/fork'; import { SmallTile } from '~/components/small-tile'; import { TextureImage } from '~/components/texture-img'; import { Tile } from '~/components/tile'; @@ -19,7 +20,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, COLORS, gitBlobUrl, gitCommitUrl, GRADIENT, GRADIENT_DANGER } from '~/lib/constants'; -import { getContributionsOfFork, getFork } from '~/server/actions/git'; +import { getContributionsOfFork } from '~/server/actions/git'; import { archiveContributions, createContributionsFromGitFiles, deleteContributions, deleteContributionsOrArchive, getContributionsOfUser, submitContributions } from '~/server/data/contributions'; import { getTextures } from '~/server/data/texture'; @@ -34,7 +35,7 @@ export default function ContributeSubmissionsPage() { const os = useOs(); const [loading, startTransition] = useTransition(); - const [hasFork, setHasFork] = useState<string | null>(null); + const [forkUrl, setForkUrl] = useState<string | null>(null); const [showHelp, helpShown] = useState(false); const [selectedContributions, setSelectedContributions] = useState<string[]>([]); @@ -61,10 +62,8 @@ export default function ContributeSubmissionsPage() { const reload = async () => { startTransition(async () => { - const fork = await getFork(); - if (!fork) return; + if (!forkUrl) return; - setHasFork(fork); getContributionsOfFork(resolution).then(updateForkContributions); }); }; @@ -147,6 +146,12 @@ export default function ContributeSubmissionsPage() { return ( <Stack gap="xs"> + + <ForkInfo + onUrlUpdate={setForkUrl} + forkUrl={forkUrl} + /> + <Group wrap="nowrap" gap="xs" @@ -262,7 +267,7 @@ export default function ContributeSubmissionsPage() { To contribute, you need to: </Text> <List ml="sm"> - <List.Item><Text fw={360}>Fork the default repository using the specified area in your <Link href='/user/me'>user page</Link>;</Text></List.Item> + <List.Item><Text fw={360}>If not already, fork the default textures repository using the "Create Fork" button;</Text></List.Item> <List.Item><Text fw={360}>Clone it to your local machine using <Link href="https://git-scm.com/" target="_blank">Git</Link> or <Link href="https://desktop.github.com/download/" target="_blank">GitHub Desktop</Link>;</Text></List.Item> <List.Item><Text fw={360}>Switch to the branch corresponding to the resolution you want to contribute to;</Text></List.Item> <List.Item><Text fw={360}>Add textures to the repository, <Text component="span" fw={700}>each texture should have the same name as the contributed texture name in the <Link href="https://github.com/faithful-mods/resources-default" target="_blank">default repository</Link></Text>;</Text></List.Item> @@ -290,18 +295,6 @@ export default function ContributeSubmissionsPage() { </Tile> )} - {hasFork === null && ( - <Group - align="center" - justify="center" - h="100px" - w="100%" - style={{ height: 'calc(81% - (2 * var(--mantine-spacing-sm) - 62px))' }} - > - <Text c="dimmed">You need to fork the default repository first</Text> - </Group> - )} - {contributions.length !== 0 && contributions.filter((c) => c.status === Object.keys(Status)[activeTab]).length === 0 && ( <Group align="center" @@ -326,7 +319,7 @@ export default function ContributeSubmissionsPage() { </Group> )} - {hasFork && ( + {forkUrl && ( <Group gap="xs"> {contributions.filter((c) => c.status === Object.keys(Status)[activeTab]).map((contribution, index) => { const texture = textures.find((t) => t.id === contribution.textureId); diff --git a/src/app/(pages)/(protected)/user/[userId]/page.tsx b/src/app/(pages)/(protected)/user/[userId]/page.tsx index 1f18dd7e..98f3933c 100644 --- a/src/app/(pages)/(protected)/user/[userId]/page.tsx +++ b/src/app/(pages)/(protected)/user/[userId]/page.tsx @@ -1,18 +1,16 @@ 'use client'; -import Link from 'next/link'; import { useParams, useRouter } from 'next/navigation'; import { useState, useTransition } from 'react'; -import { GoCheckCircle, GoStop } from 'react-icons/go'; - import { Button, Text, TextInput, Group, Stack, Badge } from '@mantine/core'; import { useForm } from '@mantine/form'; import { UserRole } from '@prisma/client'; import { signOut } from 'next-auth/react'; import { useSession } from 'next-auth/react'; +import ForkInfo from '~/components/fork'; import { TextureImage } from '~/components/texture-img'; import { Tile } from '~/components/tile'; import { useCurrentUser } from '~/hooks/use-current-user'; @@ -20,7 +18,7 @@ import { useDeviceSize } from '~/hooks/use-device-size'; import { useEffectOnce } from '~/hooks/use-effect-once'; import { BREAKPOINT_MOBILE_LARGE, GRADIENT, MAX_NAME_LENGTH, MIN_NAME_LENGTH } from '~/lib/constants'; import { notify } from '~/lib/utils'; -import { deleteFork, forkRepository, getFork } from '~/server/actions/git'; +import { deleteFork } from '~/server/actions/git'; import { getUserById } from '~/server/data/user'; import { updateUser } from '~/server/data/user'; @@ -30,6 +28,7 @@ const UserPage = () => { const params = useParams(); const user = useCurrentUser()!; const self = params.userId === 'me'; + const [forkUrl, setForkUrl] = useState<string | null>(null); const [displayedUser, setDisplayedUser] = useState<User>(); const router = useRouter(); @@ -38,8 +37,6 @@ const UserPage = () => { const [loading, startTransition] = useTransition(); const [windowWidth] = useDeviceSize(); - const [hasFork, setHasFork] = useState<string | null>(null); - const form = useForm<Pick<User, 'name' | 'image'>>({ initialValues: { name: user.name!, image: user.image! }, validate: { @@ -80,7 +77,6 @@ const UserPage = () => { if (params.userId === user?.id) router.push('/user/me'); const userId = self ? user?.id! : params.userId as string; - getFork().then(setHasFork); getUserById(userId).then(setDisplayedUser); }); }; @@ -88,7 +84,7 @@ const UserPage = () => { const handleForkDelete = async () => { startTransition(async () => { await deleteFork(); - await reload(); + setForkUrl(null); }); }; @@ -96,57 +92,6 @@ const UserPage = () => { reload(); }); - const handleSetupForkedRepository = async () => { - startTransition(async () => { - await forkRepository(); - await reload(); - }); - }; - - const forkedInfo = () => { - if (hasFork) { - return ( - <Tile p="xs" pl={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'xs' : 'md'} color="teal" mih={56}> - <Group gap="sm" mt="auto" mb="auto"> - <GoCheckCircle size={20} color="white" /> - <Group gap={3}> - <Text size="sm" c="white">Default textures repository forked: </Text> - <Text size="sm" c="white"> - <Link href={hasFork} style={{ color: 'white' }}> - {windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'link' : hasFork} - </Link> - </Text> - </Group> - </Group> - </Tile> - ); - } - - return ( - <Tile p="xs" pl={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'xs' : 'md'} color="yellow"> - <Group justify="space-between" gap="xs"> - <Group gap="sm"> - <GoStop color="black" size={20} /> - <Group gap="xs"> - <Text size="sm" c="black">Default textures repository not forked</Text> - </Group> - </Group> - - <Button - variant="outline" - color="black" - onClick={handleSetupForkedRepository} - disabled={!!hasFork} - loading={loading} - fullWidth={windowWidth <= BREAKPOINT_MOBILE_LARGE} - > - Create Fork - </Button> - </Group> - </Tile> - ); - }; - return (displayedUser && ( <Stack gap="xl"> <Group @@ -232,7 +177,7 @@ const UserPage = () => { <Stack gap="xs"> <Text fw={700}>Contributions Repository</Text> - {forkedInfo()} + <ForkInfo onUrlUpdate={setForkUrl} forkUrl={forkUrl} /> </Stack> <Stack gap="xs" mb="sm"> @@ -246,7 +191,7 @@ const UserPage = () => { borderColor: 'var(--mantine-color-red-filled)', }} > - <Group justify="space-between" style={{ opacity: hasFork ? 1 : .5 }}> + <Group justify="space-between" style={{ opacity: forkUrl ? 1 : .5 }}> <Stack gap={0}> <Text>Delete the forked repository</Text> <Text c="dimmed" size="xs">This action is irreversible, all contributions will be lost.</Text> @@ -255,7 +200,7 @@ const UserPage = () => { variant="default" style={{ color: 'var(--mantine-color-red-text)' }} onClick={handleForkDelete} - disabled={!hasFork} + disabled={!forkUrl} loading={loading} fullWidth={windowWidth <= BREAKPOINT_MOBILE_LARGE} > diff --git a/src/components/fork.tsx b/src/components/fork.tsx new file mode 100644 index 00000000..3bb06793 --- /dev/null +++ b/src/components/fork.tsx @@ -0,0 +1,83 @@ +import Link from 'next/link'; + +import { useTransition } from 'react'; + +import { GoCheckCircle, GoStop } from 'react-icons/go'; + +import { Button, Group, Text } from '@mantine/core'; + +import { useDeviceSize } from '~/hooks/use-device-size'; +import { useEffectOnce } from '~/hooks/use-effect-once'; +import { BREAKPOINT_MOBILE_LARGE } from '~/lib/constants'; +import { forkRepository, getFork } from '~/server/actions/git'; + +import { Tile } from './tile'; + +interface Props { + onUrlUpdate: (url: string | null) => void; + forkUrl: string | null; + hideIfForked?: boolean; +} + +export default function ForkInfo({ onUrlUpdate, forkUrl, hideIfForked }: Props) { + const [windowWidth] = useDeviceSize(); + const [loading, startTransition] = useTransition(); + + const handleSetupForkedRepository = async () => { + startTransition(async () => { + await forkRepository(); + const res = await getFork(); + onUrlUpdate(res); + }); + }; + + useEffectOnce(() => { + startTransition(() => { + getFork().then(onUrlUpdate); + }); + }); + + if (forkUrl && hideIfForked) return null; + + if (forkUrl) { + return ( + <Tile p="xs" pl={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'xs' : 'md'} color="teal" mih={56}> + <Group gap="sm" mt="auto" mb="auto"> + <GoCheckCircle size={20} color="white" /> + <Group gap={3}> + <Text size="sm" c="white">Default textures repository forked: </Text> + <Text size="sm" c="white"> + <Link href={forkUrl} style={{ color: 'white' }} target="_blank"> + {windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'link' : forkUrl} + </Link> + </Text> + </Group> + </Group> + </Tile> + ); + } + + return ( + <Tile p="xs" pl={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'xs' : 'md'} color="yellow"> + <Group justify="space-between" gap="xs"> + <Group gap="sm"> + <GoStop color="black" size={20} /> + <Group gap="xs"> + <Text size="sm" c="black">Default textures repository not forked</Text> + </Group> + </Group> + + <Button + variant="outline" + color="black" + onClick={handleSetupForkedRepository} + disabled={!!forkUrl} + loading={loading} + fullWidth={windowWidth <= BREAKPOINT_MOBILE_LARGE} + > + Create Fork + </Button> + </Group> + </Tile> + ); +} From 83e4c7059f4b67274083ae93e5f1c3b753cef608 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Mon, 19 Aug 2024 01:10:39 +0200 Subject: [PATCH 20/30] fix : make sure resolution branches are empty --- src/server/actions/git.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/server/actions/git.ts b/src/server/actions/git.ts index 6051f58b..6ca048b6 100644 --- a/src/server/actions/git.ts +++ b/src/server/actions/git.ts @@ -101,10 +101,23 @@ export async function forkRepository() { // create empty branches for each resolution const resolutions = Object.keys(Resolution) as Resolution[]; - const firstCommitSha = await getFirstCommit(username, GITHUB_DEFAULT_REPO_NAME); + const SHA1_EMPTY_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; // see https://github.com/orgs/community/discussions/24699 + + const res = await octokit.request('POST /repos/{owner}/{repo}/git/commits', { + owner: username, + repo: GITHUB_DEFAULT_REPO_NAME, + message: 'initial commit', + tree: SHA1_EMPTY_TREE, + parents: [], + }); for (const resolution of resolutions) { - await createBranchFromCommit(username, GITHUB_DEFAULT_REPO_NAME, resolution, firstCommitSha); + await octokit.request('POST /repos/{owner}/{repo}/git/refs', { + owner: username, + repo: GITHUB_DEFAULT_REPO_NAME, + ref: `refs/heads/${resolution}`, + sha: res.data.sha, + }); } // set x32 as default branch and delete main (x16) as it's not needed From 189677621d896106339ec815e0f27ea9055a5849 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Mon, 19 Aug 2024 01:19:35 +0200 Subject: [PATCH 21/30] feat : add ID to search filter --- src/lib/utils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 6f360844..8130df18 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -35,13 +35,14 @@ export function sortByName<T extends { name: string, id?: string | number }>(a: return a.name.localeCompare(b.name) || `${a.id}`.localeCompare(`${b.id}` ?? '') || 0; } -export function searchFilter<T extends { name: string, aliases?: string[] }>(search: string) { +export function searchFilter<T extends { id: string | number; name: string, aliases?: string[] }>(search: string) { return (item: T) => { const searchLower = search.toLowerCase(); const name = item.name.toLowerCase(); + const id = `${item.id}`.toLowerCase(); const aliases = item.aliases?.map((alias) => alias.toLowerCase()) ?? []; - return name.includes(searchLower) || aliases.some((alias) => alias.includes(searchLower)); + return id === searchLower || name.includes(searchLower) || aliases.some((alias) => alias.includes(searchLower)); }; } From 24c835934110cf97f6f6a38ba196e369e0f877b0 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Mon, 19 Aug 2024 02:19:33 +0200 Subject: [PATCH 22/30] feat : add magic wand to select vanilla texture --- .../textures/modal/texture-general.tsx | 174 +++++++++++------- src/components/fakeInputLabel.tsx | 39 ++++ 2 files changed, 146 insertions(+), 67 deletions(-) create mode 100644 src/components/fakeInputLabel.tsx diff --git a/src/app/(pages)/(protected)/council/textures/modal/texture-general.tsx b/src/app/(pages)/(protected)/council/textures/modal/texture-general.tsx index cea430bc..e3ed8765 100644 --- a/src/app/(pages)/(protected)/council/textures/modal/texture-general.tsx +++ b/src/app/(pages)/(protected)/council/textures/modal/texture-general.tsx @@ -1,12 +1,16 @@ import { useState, useTransition } from 'react'; -import { Stack, Switch, TextInput, Text, Textarea, Button, Group, Select } from '@mantine/core'; +import { PiMagicWandBold } from 'react-icons/pi'; + +import { Stack, Switch, TextInput, Text, Textarea, Button, Group, Select, ActionIcon } from '@mantine/core'; import { useForm } from '@mantine/form'; import { Resolution } from '@prisma/client'; +import { FakeInputLabel } from '~/components/fakeInputLabel'; import { TextureImage } from '~/components/texture-img'; +import { useDeviceSize } from '~/hooks/use-device-size'; import { useEffectOnce } from '~/hooks/use-effect-once'; -import { GRADIENT } from '~/lib/constants'; +import { BREAKPOINT_MOBILE_LARGE, GRADIENT } from '~/lib/constants'; import { getVanillaTextures } from '~/server/actions/faithful-pack'; import { getTextureStatus, updateTexture } from '~/server/data/texture'; @@ -25,9 +29,11 @@ export interface TextureGeneralForm { export function TextureGeneral({ texture }: TextureGeneralProps) { const [loading, startTransition] = useTransition(); + const [windowWidth] = useDeviceSize(); const [contributionsStatus, setContributionsStatus] = useState<ContributionActivationStatus[]>([]); + const [vanillaTextureSearch, setVanillaTextureSearch] = useState<string>(''); const [vanillaTexture, setVanillaTexture] = useState<string | null>(texture.vanillaTextureId); const [vanillaTextures, setVanillaTextures] = useState<FaithfulCached[]>([]); @@ -86,8 +92,8 @@ export function TextureGeneral({ texture }: TextureGeneralProps) { return ( <Stack> - <Group mt="md" wrap="nowrap" align="start"> - <Stack gap="sm" w="100%"> + <Group mt="md" wrap={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'wrap' : 'nowrap'} align="start"> + <Stack w="100%"> <TextInput w="100%" required @@ -103,83 +109,117 @@ export function TextureGeneral({ texture }: TextureGeneralProps) { {...form.getInputProps('aliases')} /> </Stack> - <Stack gap="sm" w="100%"> - <Select - w="100%" - limit={25} - label="Vanilla texture" - placeholder="Type to search or select a vanilla texture" + <Stack w="100%" gap="md"> + <FakeInputLabel + label="Vanilla Texture" description="If this texture is a vanilla texture, select the corresponding vanilla texture, contributions will be disabled" - clearable - searchable - value={vanillaTexture} - data={vanillaTextures.map((vt) => ({ value: vt.textureId, label: vt.textureName }))} - renderOption={renderMultiSelectOption} - onChange={(vanillaTexture) => { - setVanillaTexture(vanillaTexture); - setContributionsStatus([ - { resolution: null, status: vanillaTexture === null }, - ...Object.keys(Resolution).flatMap((res) => ({ resolution: res as Resolution, status: vanillaTexture === null })), - ]); - }} - /> + > + <Group gap="xs" wrap="nowrap"> + <ActionIcon + variant="light" + className="navbar-icon-fix" + onClick={() => { + const name = form.getValues().name; + const vanillaTexture = vanillaTextures.find((vt) => vt.textureName === name)?.textureId ?? null; + + setVanillaTexture(vanillaTexture); + setVanillaTextureSearch(name ?? ''); + setContributionsStatus([ + { resolution: null, status: vanillaTexture === null }, + ...Object.keys(Resolution).flatMap((res) => ({ resolution: res as Resolution, status: vanillaTexture === null })), + ]); + }} + > + <PiMagicWandBold /> + </ActionIcon> + <Select + w="100%" + + placeholder="Type to search or select a vanilla texture" + limit={25} + + clearable + + data={vanillaTextures.map((vt) => ({ value: vt.textureId, label: vt.textureName }))} + value={vanillaTexture} + defaultValue={vanillaTextures.find((vt) => vt.textureId === vanillaTexture)?.textureName} + renderOption={renderMultiSelectOption} + + onChange={(vanillaTexture) => { + setVanillaTexture(vanillaTexture); + setVanillaTextureSearch(vanillaTextures.find((vt) => vt.textureId === vanillaTexture)?.textureName ?? ''); + setContributionsStatus([ + { resolution: null, status: vanillaTexture === null }, + ...Object.keys(Resolution).flatMap((res) => ({ resolution: res as Resolution, status: vanillaTexture === null })), + ]); + }} - <Stack gap="xs"> - <Stack gap={5}> - <Text size="var(--input-label-size, var(--mantine-font-size-sm))"> - Contributions - </Text> - <Text c="dimmed" size="var(--input-description-size, calc(var(--mantine-font-size-sm) - calc(.125rem * var(--mantine-scale))))"> - Users will not be able to contribute to this texture on the unselected resolutions - </Text> - </Stack> - <Switch - label="Contributions enabled" - disabled={vanillaTexture !== null} - color="blue" - onLabel="ON" - offLabel="OFF" - checked={contributionsStatus.find((s) => s.resolution === null)?.status} - onChange={(e) => { - setContributionsStatus([ - { resolution: null, status: e.currentTarget.checked }, - ...Object.keys(Resolution).flatMap((res) => ({ resolution: res as Resolution, status: e.currentTarget.checked })), - ]); - }} - /> - - {(Object.keys(Resolution) as Resolution[]).map((res) => + searchable + searchValue={vanillaTextureSearch} + onSearchChange={setVanillaTextureSearch} + /> + </Group> + </FakeInputLabel> + + <FakeInputLabel + label="Contributions" + description="Users will not be able to contribute to this texture" + gap="var(--mantine-spacing-xs)" + > + <Stack> <Switch - key={res} - label={res} + label="Enable contributions" + disabled={vanillaTexture !== null} color="blue" onLabel="ON" offLabel="OFF" - disabled={!contributionsStatus.find((s) => s.resolution === null)?.status} - checked={contributionsStatus.find((s) => s.resolution === res)?.status} + checked={contributionsStatus.find((s) => s.resolution === null)?.status} onChange={(e) => { - setContributionsStatus(contributionsStatus.map((s) => s.resolution === res ? { resolution: res, status: e.currentTarget.checked } : s)); + setContributionsStatus([ + { resolution: null, status: e.currentTarget.checked }, + ...Object.keys(Resolution).flatMap((res) => ({ resolution: res as Resolution, status: e.currentTarget.checked })), + ]); }} /> - )} - </Stack> + <Stack gap="xs"> + <Text c="dimmed" size="var(--input-description-size, calc(var(--mantine-font-size-sm) - calc(.125rem * var(--mantine-scale))))"> + Enable/disable contributions for specific resolutions + </Text> + + <Group> + {(Object.keys(Resolution) as Resolution[]).map((res) => + <Switch + key={res} + label={res} + color="blue" + onLabel="ON" + offLabel="OFF" + disabled={!contributionsStatus.find((s) => s.resolution === null)?.status} + checked={contributionsStatus.find((s) => s.resolution === res)?.status} + onChange={(e) => { + setContributionsStatus(contributionsStatus.map((s) => s.resolution === res ? { resolution: res, status: e.currentTarget.checked } : s)); + }} + /> + )} + </Group> + </Stack> + </Stack> + </FakeInputLabel> </Stack> </Group> - <Group - justify='end' + <Button + mt="md" + fullWidth + variant="gradient" + gradient={GRADIENT} + onClick={() => handleSave()} + disabled={loading || !form.isValid()} + loading={loading} > - <Button - variant="gradient" - gradient={GRADIENT} - onClick={() => handleSave()} - disabled={loading || !form.isValid()} - loading={loading} - > - Save - </Button> - </Group> + Save + </Button> </Stack> ); } diff --git a/src/components/fakeInputLabel.tsx b/src/components/fakeInputLabel.tsx new file mode 100644 index 00000000..5021e9d1 --- /dev/null +++ b/src/components/fakeInputLabel.tsx @@ -0,0 +1,39 @@ +import { Stack, Text } from '@mantine/core'; + +interface Props { + children: React.ReactNode; + label: string; + description?: string; + gap?: number | string; +} + +export function FakeInputLabel({ gap, label, description, children }: Props) { + return ( + <Stack gap={0} mt={2}> + <Stack gap={0}> + <Text + lh="var(--mantine-line-height)" + size="var(--mantine-font-size-sm)" + fw={500} + > + {label} + </Text> + <Text + lh={1.2} + size="var(--input-description-size, calc(var(--mantine-font-size-sm) - calc(.125rem * var(--mantine-scale))))" + c="dimmed" + > + {description} + </Text> + </Stack> + + <div + style={{ + marginTop: gap ? gap : description ? 'calc(var(--mantine-spacing-xs) / 2)' : 1, + }} + > + {children} + </div> + </Stack> + ); +} From bcb013377f5311ddf727eb46a91922fcd4d91ccd Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Mon, 19 Aug 2024 03:00:44 +0200 Subject: [PATCH 23/30] feat : add texture hash & vanilla mention to popup --- src/components/texture-contribution.tsx | 77 ++++++++++++++++++++----- src/components/texture.tsx | 40 ++++++++++--- 2 files changed, 93 insertions(+), 24 deletions(-) diff --git a/src/components/texture-contribution.tsx b/src/components/texture-contribution.tsx index 24d5a44c..792b8894 100644 --- a/src/components/texture-contribution.tsx +++ b/src/components/texture-contribution.tsx @@ -1,7 +1,9 @@ +import Link from 'next/link'; + import { useMemo, useState } from 'react'; import type { RefObject } from 'react'; -import { GoHash, GoPeople, GoPerson } from 'react-icons/go'; +import { GoHash, GoLinkExternal, GoLog, GoPeople, GoPerson } from 'react-icons/go'; import { PiApproximateEquals } from 'react-icons/pi'; import { Avatar, Group, Stack, Text } from '@mantine/core'; @@ -9,7 +11,7 @@ import { Avatar, Group, Stack, Text } from '@mantine/core'; import { SmallTile } from '~/components/small-tile'; import { TextureImage } from '~/components/texture-img'; import { useEffectOnce } from '~/hooks/use-effect-once'; -import { getVanillaTextureSrc } from '~/lib/utils'; +import { getVanillaResolution, getVanillaTextureSrc } from '~/lib/utils'; import { getLatestVanillaTextureContribution } from '~/server/actions/faithful-pack'; import type { Contribution, Resolution, Texture } from '@prisma/client'; @@ -55,6 +57,8 @@ export function GalleryTextureWithContribution({ const [vanillaContribution, setVanillaContribution] = useState<FPStoredContribution | null>(null); + const filteredVanillaCoAuthors = useMemo(() => vanillaContribution?.coAuthors.filter((ca) => ca.username !== vanillaContribution?.owner.username) ?? [], [vanillaContribution]); + useEffectOnce(() => { if (!texture.vanillaTextureId || resolution === 'x16') return; @@ -76,7 +80,7 @@ export function GalleryTextureWithContribution({ boxShadow: 'none', }} > - <Stack gap={2} align="start" miw={400} maw={400}> + <Stack gap={2} align="start" miw={450} maw={450}> <SmallTile color="gray"> <Text fw={500} ta="center">{texture.name}</Text> </SmallTile> @@ -97,14 +101,14 @@ export function GalleryTextureWithContribution({ </SmallTile> </Group> )} - {resolution !== 'x16' && (contribution && contribution.coAuthors.length > 0) || (vanillaContribution && vanillaContribution.coAuthors.length > 0) && ( + {resolution !== 'x16' && (contribution && contribution.coAuthors.length > 0) || (filteredVanillaCoAuthors.length > 0) && ( <Group gap={2} w="100%" wrap="nowrap" align="start"> <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> <GoPeople /> </SmallTile> <SmallTile color="gray"> <Text size="xs"> - {contribution ? contribution.coAuthors.map((ca) => ca.name).join(', ') : vanillaContribution?.coAuthors.map((ca) => ca.username).join(', ')} + {contribution ? contribution.coAuthors.map((ca) => ca.name).join(', ') : filteredVanillaCoAuthors.map((ca) => ca.username).join(', ')} </Text> </SmallTile> </Group> @@ -121,16 +125,59 @@ export function GalleryTextureWithContribution({ </SmallTile> </Group> )} - <Group gap={2} w="100%" wrap="nowrap" align="start"> - <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> - <GoHash /> - </SmallTile> - <SmallTile color="gray"> - <Text size="xs" c="dimmed"> - ID: {texture.id} - </Text> - </SmallTile> - </Group> + {resolution !== 'x16' && texture.vanillaTextureId && ( + <Group gap={2} w="100%" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }} > + <GoLinkExternal /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs"> + <Link + href={`https://webapp.faithfulpack.net/gallery/java/${getVanillaResolution(resolution)}/java-snapshot/all?show=${texture.vanillaTextureId}`} + target="_blank" + > + See in the Faithful Webapp + </Link> + </Text> + </SmallTile> + </Group> + )} + {!texture.vanillaTextureId && ( + <Group gap={2} w="100%" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <GoHash /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs" c="dimmed"> + Texture ID: {texture.id} + </Text> + </SmallTile> + </Group> + )} + {(resolution === 'x16' && !texture.vanillaTextureId || !contribution && !vanillaContribution) && ( + <Group gap={2} w="100%" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <GoLog /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs" c="dimmed"> + {texture.hash} + </Text> + </SmallTile> + </Group> + )} + {texture.vanillaTextureId && ( + <Group gap={2} w="100%" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <GoHash /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs" c="dimmed"> + Vanilla Texture ID: {texture.vanillaTextureId} + </Text> + </SmallTile> + </Group> + )} {texture.aliases.length > 0 && ( <Group gap={2} w="100%" wrap="nowrap" align="start"> <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> diff --git a/src/components/texture.tsx b/src/components/texture.tsx index 9b597a8c..683a2553 100644 --- a/src/components/texture.tsx +++ b/src/components/texture.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import type { RefObject } from 'react'; -import { GoHash } from 'react-icons/go'; +import { GoAlert, GoHash, GoLog } from 'react-icons/go'; import { PiApproximateEquals } from 'react-icons/pi'; import { Group, Stack, Text } from '@mantine/core'; @@ -48,27 +48,49 @@ export function GalleryTexture({ boxShadow: 'none', }} > - <Stack gap={2} align="start" miw={400} maw={400}> - <SmallTile> + <Stack gap={2} align="start" miw={450} maw={450}> + <SmallTile color="gray"> <Text fw={500} ta="center">{texture.name}</Text> </SmallTile> + {texture.vanillaTextureId && ( + <Group gap={2} w="100%" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }} > + <GoAlert color="orange" /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs"> + Vanilla texture : {texture.vanillaTextureId} + </Text> + </SmallTile> + </Group> + )} <Group gap={2} w="100%" wrap="nowrap" align="start"> - <SmallTile className="navbar-icon-fix" style={{ '--size': '28px' }}> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> <GoHash /> </SmallTile> - <SmallTile> - <Text size="xs" c="dimmed"> + <SmallTile color="gray"> + <Text size="xs"> ID: {texture.id} </Text> </SmallTile> </Group> + <Group gap={2} w="100%" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <GoLog /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs"> + {texture.hash} + </Text> + </SmallTile> + </Group> {texture.aliases.length > 0 && ( <Group gap={2} w="100%" wrap="nowrap" align="start"> - <SmallTile className="navbar-icon-fix" style={{ '--size': '28px' }}> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> <PiApproximateEquals /> </SmallTile> - <SmallTile> - <Text size="xs" c="dimmed"> + <SmallTile color="gray"> + <Text size="xs"> {texture.aliases.join(', ')} </Text> </SmallTile> From f8b8bf666ab5c6e4f4841ef348fb912e9fe3ead7 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Mon, 19 Aug 2024 03:42:27 +0200 Subject: [PATCH 24/30] feat : add vanilla texture & disabled contribution to contributions --- .../(pages)/(protected)/contribute/page.tsx | 216 ++++++++++++------ src/server/data/texture.ts | 8 +- 2 files changed, 147 insertions(+), 77 deletions(-) diff --git a/src/app/(pages)/(protected)/contribute/page.tsx b/src/app/(pages)/(protected)/contribute/page.tsx index 1bf62447..814a142f 100644 --- a/src/app/(pages)/(protected)/contribute/page.tsx +++ b/src/app/(pages)/(protected)/contribute/page.tsx @@ -4,7 +4,7 @@ import Link from 'next/link'; import { useCallback, useEffect, useState, useTransition } from 'react'; -import { GoCommit, GoHash, GoHourglass, GoQuestion, GoRelFilePath } from 'react-icons/go'; +import { GoAlert, GoCommit, GoHash, GoHourglass, GoQuestion, GoRelFilePath } from 'react-icons/go'; import { IoReload } from 'react-icons/io5'; import { LuArrowUpDown } from 'react-icons/lu'; @@ -24,9 +24,9 @@ import { getContributionsOfFork } from '~/server/actions/git'; import { archiveContributions, createContributionsFromGitFiles, deleteContributions, deleteContributionsOrArchive, getContributionsOfUser, submitContributions } from '~/server/data/contributions'; import { getTextures } from '~/server/data/texture'; -import type { Texture } from '@prisma/client'; import type { GitFile } from '~/server/actions/git'; import type { GetContributionsOfUser } from '~/server/data/contributions'; +import type { GetTextures } from '~/server/data/texture'; import './styles.scss'; @@ -44,7 +44,7 @@ export default function ContributeSubmissionsPage() { const prevRes = usePrevious(resolution); const [contributions, setContributions] = useState<GetContributionsOfUser[]>([]); - const [textures, setTextures] = useState<Texture[]>([]); + const [textures, setTextures] = useState<GetTextures[]>([]); const [groupRef, setGroupRef] = useState<HTMLDivElement | null>(null); const [controlsRefs, setControlsRefs] = useState<Record<string, HTMLButtonElement | null>>({}); @@ -52,9 +52,24 @@ export default function ContributeSubmissionsPage() { const [windowWidth] = useDeviceSize(); useHotkeys([ - ['mod+a', () => setSelectedContributions(contributions.filter((c) => c.status === Object.keys(Status)[activeTab]).map((c) => c.id))], + [ + 'mod+a', + () => setSelectedContributions(contributions + .filter(canContributionBeSubmitted) + .filter((c) => c.status === Object.keys(Status)[activeTab]).map((c) => c.id)), + ], ]); + const canContributionBeSubmitted = useCallback((contribution: GetContributionsOfUser) => { + const texture = textures.find((t) => t.id === contribution.textureId); + if (!texture) return false; + + const disabledResolution = texture.disabledContributions.find((dc) => dc.resolution === resolution); + const allResolutionsDisabled = texture.disabledContributions.find((dc) => dc.resolution === null); + + return !disabledResolution && !allResolutionsDisabled; + }, [resolution, textures]); + const setControlRef = (index: number) => (node: HTMLButtonElement) => { controlsRefs[index] = node; setControlsRefs(controlsRefs); @@ -268,7 +283,7 @@ export default function ContributeSubmissionsPage() { </Text> <List ml="sm"> <List.Item><Text fw={360}>If not already, fork the default textures repository using the "Create Fork" button;</Text></List.Item> - <List.Item><Text fw={360}>Clone it to your local machine using <Link href="https://git-scm.com/" target="_blank">Git</Link> or <Link href="https://desktop.github.com/download/" target="_blank">GitHub Desktop</Link>;</Text></List.Item> + <List.Item><Text fw={360}>Clone it to your local machine using <Link href="https://git-scm.com/" target="_blank">Git</Link> or <Link href="https://desktop.github.com/download/" target="_blank">GitHub Desktop</Link> (recommended);</Text></List.Item> <List.Item><Text fw={360}>Switch to the branch corresponding to the resolution you want to contribute to;</Text></List.Item> <List.Item><Text fw={360}>Add textures to the repository, <Text component="span" fw={700}>each texture should have the same name as the contributed texture name in the <Link href="https://github.com/faithful-mods/resources-default" target="_blank">default repository</Link></Text>;</Text></List.Item> <List.Item><Text fw={360}>Commit your changes and push them to your fork;</Text></List.Item> @@ -327,19 +342,29 @@ export default function ContributeSubmissionsPage() { const repository = contribution.filepath.split('/')[4]!; const commitSha = contribution.filepath.split('/')[5]!; + const disabledResolution = texture?.disabledContributions.find((dc) => dc.resolution === resolution); + const allResolutionsDisabled = texture?.disabledContributions.find((dc) => dc.resolution === null); + return ( ( <TextureImage key={index} src={contribution.filepath} alt={contribution.filename} - onClick={() => setSelectedContributions((prev) => { - if (prev.includes(contribution.id)) return prev.filter((id) => id !== contribution.id); - return [...prev, contribution.id]; - })} + onClick={() => { + if (!canContributionBeSubmitted(contribution)) return; + + setSelectedContributions((prev) => { + if (prev.includes(contribution.id)) return prev.filter((id) => id !== contribution.id); + return [...prev, contribution.id]; + }); + }} + styles={{ boxShadow: selectedContributions.includes(contribution.id) ? '0 0 0 2px var(--mantine-color-teal-filled)' : 'none', + cursor: canContributionBeSubmitted(contribution) ? 'pointer' : 'not-allowed', }} + popupStyles={{ backgroundColor: 'transparent', padding: 0, @@ -347,92 +372,133 @@ export default function ContributeSubmissionsPage() { boxShadow: 'none', }} > - <Group gap={2}> - {texture && ( - <SmallTile color="gray" w={125} h="100%"> - <Group w="100%" h="100%" justify="center" align="center"> - <TextureImage - src={texture.filepath} - alt={texture.name} - mcmeta={texture.mcmeta} - size={115} - /> - </Group> - </SmallTile> - )} - <Stack gap={2} align="start" miw={400} maw={400}> - <SmallTile color="gray"> - <Text fw={500} ta="center">{texture?.name}</Text> - </SmallTile> - - <Group gap={2} w="100%" wrap="nowrap" align="start"> - <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> - <GoCommit /> + <Stack gap={2}> + <Group gap={2}> + {texture && ( + <SmallTile color="gray" w={125} h="100%"> + <Group w="100%" h="100%" justify="center" align="center"> + <TextureImage + src={texture.filepath} + alt={texture.name} + mcmeta={texture.mcmeta} + size={115} + /> + </Group> </SmallTile> + )} + <Stack gap={2} align="start" miw={468} maw={468}> <SmallTile color="gray"> - <Text size="xs"> - <a - href={gitCommitUrl({ orgOrUser, repository, commitSha })} - target="_blank" - rel="noreferrer" - > - {commitSha} - </a> - </Text> + <Text fw={500} ta="center">{texture?.name}</Text> </SmallTile> - </Group> - - <Group gap={2} w="100%" wrap="nowrap" align="start"> - <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> - <GoRelFilePath /> - </SmallTile> - <SmallTile color="gray"> - <Text size="xs"> - <a - href={gitBlobUrl({ orgOrUser, repository, branchOrCommit: commitSha, path: contribution.filename })} - target="_blank" - rel="noreferrer" - > - {contribution.filename} - </a> - </Text> - </SmallTile> - </Group> - <Group gap={2} w="100%"> - <Group gap={2} w="calc((100% - 4px) / 3)" wrap="nowrap" align="start"> + <Group gap={2} w="100%" wrap="nowrap" align="start"> <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> - <LuArrowUpDown /> + <GoCommit /> </SmallTile> <SmallTile color="gray"> <Text size="xs"> - {contribution.poll.upvotes.length - contribution.poll.downvotes.length} + <a + href={gitCommitUrl({ orgOrUser, repository, commitSha })} + target="_blank" + rel="noreferrer" + > + {commitSha} + </a> </Text> </SmallTile> </Group> - <Group gap={2} w="calc((100% - 4px) / 3)" wrap="nowrap" align="start"> + + <Group gap={2} w="100%" wrap="nowrap" align="start"> <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> - <GoHash /> + <GoRelFilePath /> </SmallTile> <SmallTile color="gray"> <Text size="xs"> - {contribution.textureId} + <a + href={gitBlobUrl({ orgOrUser, repository, branchOrCommit: commitSha, path: contribution.filename })} + target="_blank" + rel="noreferrer" + > + {contribution.filename} + </a> </Text> </SmallTile> </Group> - <Group gap={2} w="calc((100% - 4px) / 3)" wrap="nowrap" align="start"> - <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> - <GoHourglass /> - </SmallTile> - <SmallTile color="gray"> - <Text size="xs"> - {contribution.status} - </Text> - </SmallTile> + + <Group gap={2} w="100%"> + <Group gap={2} w="calc((100% - 4px) / 3)" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <LuArrowUpDown /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs"> + {contribution.poll.upvotes.length - contribution.poll.downvotes.length} + </Text> + </SmallTile> + </Group> + <Group gap={2} w="calc((100% - 4px) / 3)" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <GoHash /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs"> + {contribution.textureId} + </Text> + </SmallTile> + </Group> + <Group gap={2} w="calc((100% - 4px) / 3)" wrap="nowrap" align="start"> + <SmallTile color="gray" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <GoHourglass /> + </SmallTile> + <SmallTile color="gray"> + <Text size="xs"> + {contribution.status} + </Text> + </SmallTile> + </Group> </Group> + </Stack> + </Group> + + {texture && texture.vanillaTextureId && ( + <Group gap={2} w="100%" wrap="nowrap" align="start"> + <SmallTile color="yellow" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <GoAlert color="black" /> + </SmallTile> + <SmallTile color="yellow"> + <Text size="xs" c="black"> + This texture is a vanilla texture, contributions should be done from the officials Discords channels. + </Text> + </SmallTile> + </Group> + )} + + {texture && !texture.vanillaTextureId && disabledResolution && !allResolutionsDisabled && ( + <Group gap={2} w="100%" wrap="nowrap" align="start"> + <SmallTile color="red" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <GoAlert color="black" /> + </SmallTile> + <SmallTile color="red"> + <Text size="xs" c="black"> + This texture does not accept contributions for the {resolution} resolution. + </Text> + </SmallTile> + </Group> + )} + + {texture && !texture.vanillaTextureId && allResolutionsDisabled && ( + <Group gap={2} w="100%" wrap="nowrap" align="start"> + <SmallTile color="red" className="navbar-icon-fix" style={{ '--size': '28px' }}> + <GoAlert color="black" /> + </SmallTile> + <SmallTile color="red"> + <Text size="xs" c="black"> + This texture does not accept contributions for any resolution. + </Text> + </SmallTile> </Group> - </Stack> - </Group> + )} + </Stack> </TextureImage> ) ); diff --git a/src/server/data/texture.ts b/src/server/data/texture.ts index 2b2dac01..1c9479a6 100644 --- a/src/server/data/texture.ts +++ b/src/server/data/texture.ts @@ -9,13 +9,17 @@ import { db } from '~/lib/db'; import { remove } from '../actions/files'; import type { ContributionDeactivation, Texture } from '@prisma/client'; -import type { ContributionActivationStatus, Progression, TextureMCMETA } from '~/types'; +import type { ContributionActivationStatus, Prettify, Progression, TextureMCMETA } from '~/types'; import '~/lib/polyfills'; // GET -export async function getTextures(): Promise<(Texture & { disabledContributions: ContributionDeactivation[] })[]> { +export type GetTextures = Prettify<Texture & { + disabledContributions: ContributionDeactivation[]; +}>; + +export async function getTextures(): Promise<GetTextures[]> { return db.texture.findMany({ include: { disabledContributions: true } }); } From daec0ada473c4cc3640a536c5d319674603a63e3 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Mon, 19 Aug 2024 03:44:15 +0200 Subject: [PATCH 25/30] fix : clean up unused code --- src/server/actions/git.ts | 40 --------------------------------------- 1 file changed, 40 deletions(-) diff --git a/src/server/actions/git.ts b/src/server/actions/git.ts index 6ca048b6..7db5f200 100644 --- a/src/server/actions/git.ts +++ b/src/server/actions/git.ts @@ -216,24 +216,6 @@ async function setDefaultBranch(owner: string, repo: string, branch: string) { }); } -async function renameBranch(owner: string, repo: string, oldBranch: string, newBranch: string) { - const octokit = await getOctokit(); - const { data } = await octokit.git.getRef({ - owner, - repo, - ref: `heads/${oldBranch}`, - }); - - await octokit.git.createRef({ - owner, - repo, - ref: `refs/heads/${newBranch}`, - sha: data.object.sha, - }); - - await deleteBranch(owner, repo, oldBranch); -} - async function deleteBranch(owner: string, repo: string, branch: string) { const octokit = await getOctokit(); await octokit.git.deleteRef({ @@ -243,28 +225,6 @@ async function deleteBranch(owner: string, repo: string, branch: string) { }); } -async function createBranchFromCommit(owner: string, repo: string, branch: string, commitSha: string) { - const octokit = await getOctokit(); - await octokit.git.createRef({ - owner, - repo, - ref: `refs/heads/${branch}`, - sha: commitSha, - }); -} - -async function getFirstCommit(owner: string, repo: string) { - const octokit = await getOctokit(); - const { data } = await octokit.repos.listCommits({ - owner, repo, - sha: 'main', - per_page: 1, - page: 1, - }); - - return data[0]!.sha; -} - async function setBranchToCommit(commitSha: string, branch: string) { const octokit = await getOctokit(); await octokit.git.updateRef({ From 33189c60332f2c71df5f85dd4a58835eb4ad9542 Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Mon, 19 Aug 2024 03:52:00 +0200 Subject: [PATCH 26/30] feat : use mod unique id instead of cuid for pagination --- src/app/(pages)/mods/[modId]/gallery/page.tsx | 4 ++-- src/app/(pages)/mods/[modId]/layout.tsx | 6 +++--- src/app/(pages)/mods/[modId]/page.tsx | 10 +++++----- src/app/(pages)/mods/page.tsx | 2 +- src/server/data/mods-version.ts | 11 +++++++---- src/server/data/mods.ts | 4 ++++ 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/app/(pages)/mods/[modId]/gallery/page.tsx b/src/app/(pages)/mods/[modId]/gallery/page.tsx index 13ded730..0d6a51d3 100644 --- a/src/app/(pages)/mods/[modId]/gallery/page.tsx +++ b/src/app/(pages)/mods/[modId]/gallery/page.tsx @@ -14,7 +14,7 @@ import { useEffectOnce } from '~/hooks/use-effect-once'; import { BREAKPOINT_MOBILE_LARGE, BREAKPOINT_TABLET, ITEMS_PER_PAGE, ITEMS_PER_ROW } from '~/lib/constants'; import { searchFilter } from '~/lib/utils'; import { getLatestContributionsOfModVersion } from '~/server/data/contributions'; -import { getModVersionFromMod } from '~/server/data/mods-version'; +import { getModVersionFromModForgeId } from '~/server/data/mods-version'; import { getTexturesFromModVersion } from '~/server/data/texture'; import type { ModVersion, Texture } from '@prisma/client'; @@ -48,7 +48,7 @@ export default function ModGalleryPage() { const texturesGroupRef = useRef<HTMLDivElement>(null); useEffectOnce(() => { - getModVersionFromMod(modId).then((versions) => { + getModVersionFromModForgeId(modId).then((versions) => { setModVersions(versions); setModVersionShown(versions[0]?.id ?? null); }); diff --git a/src/app/(pages)/mods/[modId]/layout.tsx b/src/app/(pages)/mods/[modId]/layout.tsx index 0c49093c..0197c14d 100644 --- a/src/app/(pages)/mods/[modId]/layout.tsx +++ b/src/app/(pages)/mods/[modId]/layout.tsx @@ -16,7 +16,7 @@ import { TextureImage } from '~/components/texture-img'; import { useDeviceSize } from '~/hooks/use-device-size'; import { useEffectOnce } from '~/hooks/use-effect-once'; import { BREAKPOINT_MOBILE_LARGE } from '~/lib/constants'; -import { getModsFromIds, getModDownloads } from '~/server/data/mods'; +import { getModDownloads, getModFromForgeId } from '~/server/data/mods'; import type { Mod } from '@prisma/client'; import type { Downloads } from '~/types'; @@ -35,8 +35,8 @@ export default function ModLayout({ children }: { children: React.ReactNode }) { useEffectOnce(() => { if (!modId) return; - getModsFromIds([modId]) - .then((mod) => setMod(mod[0] ?? null)) + getModFromForgeId(modId) + .then(setMod) .then(() => getModDownloads(modId).then(setDownloads)) .finally(() => setLoading(false)); }); diff --git a/src/app/(pages)/mods/[modId]/page.tsx b/src/app/(pages)/mods/[modId]/page.tsx index 931d50ba..6d1469a6 100644 --- a/src/app/(pages)/mods/[modId]/page.tsx +++ b/src/app/(pages)/mods/[modId]/page.tsx @@ -14,8 +14,8 @@ import { Tile } from '~/components/tile'; import { useDeviceSize } from '~/hooks/use-device-size'; import { useEffectOnce } from '~/hooks/use-effect-once'; import { BREAKPOINT_MOBILE_LARGE, ITEMS_PER_PAGE, RESOLUTIONS_COLORS, EMPTY_PROGRESSION, ITEMS_PER_PAGE_DEFAULT } from '~/lib/constants'; -import { getModsFromIds } from '~/server/data/mods'; -import { getModVersionFromMod, getModVersionProgressionFromMod } from '~/server/data/mods-version'; +import { getModFromForgeId } from '~/server/data/mods'; +import { getModVersionFromModForgeId, getModVersionProgressionFromModForgeId } from '~/server/data/mods-version'; import type { Mod, ModVersion } from '@prisma/client'; import type { Progression } from '~/types'; @@ -107,9 +107,9 @@ export default function ModPage() { }, [search, versions]); useEffectOnce(() => { - getModsFromIds([modId]).then((mod) => setMod(mod[0] ?? null)); - getModVersionProgressionFromMod(modId).then(setProgressions); - getModVersionFromMod(modId).then((versions) => { + getModFromForgeId(modId).then(setMod); + getModVersionProgressionFromModForgeId(modId).then(setProgressions); + getModVersionFromModForgeId(modId).then((versions) => { setVersions(versions); setFilteredVersions(versions); }); diff --git a/src/app/(pages)/mods/page.tsx b/src/app/(pages)/mods/page.tsx index e25648f0..0be00c6d 100644 --- a/src/app/(pages)/mods/page.tsx +++ b/src/app/(pages)/mods/page.tsx @@ -280,7 +280,7 @@ export default function Mods() { modsShown[activePage - 1] && modsShown[activePage - 1]?.map((m) => ( <Tile key={m.id} - onClick={() => router.push(`/mods/${m.id}`)} + onClick={() => router.push(`/mods/${m.forgeId}`)} className="cursor-pointer mod-card" > <Stack gap="xs"> diff --git a/src/server/data/mods-version.ts b/src/server/data/mods-version.ts index 3ba6b4e3..b10e130d 100644 --- a/src/server/data/mods-version.ts +++ b/src/server/data/mods-version.ts @@ -42,8 +42,11 @@ export async function getModVersionsWithModpacks(modId: string): Promise<ModVers return res; } -export async function getModVersionFromMod(modId: string): Promise<ModVersion[]> { - return db.modVersion.findMany({ where: { modId } }).then((res) => res.sort((a, b) => sortBySemver(a.version, b.version))); +export async function getModVersionFromModForgeId(forgeId: string): Promise<ModVersion[]> { + const mod = await db.mod.findFirst({ where: { forgeId } }); + if (!mod) return []; + + return db.modVersion.findMany({ where: { modId: mod.id } }); } export async function getModVersions(): Promise<ModVersion[]> { @@ -69,8 +72,8 @@ export async function getModsVersionsFromResources(resourceIds: string[]): Promi .then((res) => res.map((modVer) => ({ ...modVer, resources: modVer.resources.map((r) => r.id) }))); } -export async function getModVersionProgressionFromMod(modId: string): Promise<Record<string, Progression>> { - const modVersions = await db.modVersion.findMany({ where: { modId }, select: { id: true } }); +export async function getModVersionProgressionFromModForgeId(forgeId: string): Promise<Record<string, Progression>> { + const modVersions = await getModVersionFromModForgeId(forgeId); const modVersionsIds = modVersions.map((mv) => mv.id); const progressions: Record<string, Progression> = {}; diff --git a/src/server/data/mods.ts b/src/server/data/mods.ts index fef5d8cc..f68f5481 100644 --- a/src/server/data/mods.ts +++ b/src/server/data/mods.ts @@ -16,6 +16,10 @@ import type { Downloads } from '~/types'; // GET +export async function getModFromForgeId(forgeId: string): Promise<Mod | null> { + return db.mod.findFirst({ where: { forgeId } }); +} + export async function getModsFromIds(ids: string[]): Promise<Mod[]> { return db.mod.findMany({ where: { id: { in: ids } } }); } From e7fd04e652c4f61fa0e065459bd6e7468cad008d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 03:53:05 +0200 Subject: [PATCH 27/30] deps-dev : bump tailwindcss from 3.4.8 to 3.4.10 (#136) Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 3.4.8 to 3.4.10. - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/v3.4.10/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.4.8...v3.4.10) --- updated-dependencies: - dependency-name: tailwindcss dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 9 ++++----- package.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 82fc62eb..52c0313d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,7 +63,7 @@ "stylelint": "^16.8.1", "stylelint-config-standard-scss": "^13.1.0", "stylelint-scss": "^6.5.0", - "tailwindcss": "^3.4.8", + "tailwindcss": "^3.4.10", "tsx": "^4.16.5", "typescript": "^5.5.4" } @@ -7121,10 +7121,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.8", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.8.tgz", - "integrity": "sha512-GkP17r9GQkxgZ9FKHJQEnjJuKBcbFhMFzKu5slmN6NjlCuFnYJMQ8N4AZ6VrUyiRXlDtPKHkesuQ/MS913Nvdg==", - "license": "MIT", + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", + "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", diff --git a/package.json b/package.json index 9c4895ad..04a5213d 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "stylelint": "^16.8.1", "stylelint-config-standard-scss": "^13.1.0", "stylelint-scss": "^6.5.0", - "tailwindcss": "^3.4.8", + "tailwindcss": "^3.4.10", "tsx": "^4.16.5", "typescript": "^5.5.4" } From 86ad5415eaffb06dc28e58dab27a273ea3a4820c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 03:59:27 +0200 Subject: [PATCH 28/30] deps-dev : bump eslint-plugin-unused-imports from 3.2.0 to 4.1.3 (#127) Bumps [eslint-plugin-unused-imports](https://github.com/sweepline/eslint-plugin-unused-imports) from 3.2.0 to 4.1.3. - [Commits](https://github.com/sweepline/eslint-plugin-unused-imports/commits/v4.1.3) --- updated-dependencies: - dependency-name: eslint-plugin-unused-imports dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 27 ++++++--------------------- package.json | 2 +- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 52c0313d..6a27ce30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "cross-env": "^7.0.3", "eslint": "^8.57.0", "eslint-config-next": "^14.2.5", - "eslint-plugin-unused-imports": "^3.2.0", + "eslint-plugin-unused-imports": "^4.1.3", "postcss": "^8.4.39", "postcss-preset-mantine": "^1.17.0", "postcss-simple-vars": "^7.0.1", @@ -3392,19 +3392,13 @@ } }, "node_modules/eslint-plugin-unused-imports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.2.0.tgz", - "integrity": "sha512-6uXyn6xdINEpxE1MtDjxQsyXB37lfyO2yKGVVgtD7WEWQGORSOZjgrD6hBhvGv4/SO+TOlS+UnC6JppRqbuwGQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.3.tgz", + "integrity": "sha512-lqrNZIZjFMUr7P06eoKtQLwyVRibvG7N+LtfKtObYGizAAGrcqLkc3tDx+iAik2z7q0j/XI3ihjupIqxhFabFA==", "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" + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^9.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@typescript-eslint/eslint-plugin": { @@ -3412,15 +3406,6 @@ } } }, - "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 04a5213d..cb645092 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "cross-env": "^7.0.3", "eslint": "^8.57.0", "eslint-config-next": "^14.2.5", - "eslint-plugin-unused-imports": "^3.2.0", + "eslint-plugin-unused-imports": "^4.1.3", "postcss": "^8.4.39", "postcss-preset-mantine": "^1.17.0", "postcss-simple-vars": "^7.0.1", From 2dc7fbb4a2dfd41d4607ae382539516d389f2d5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 03:59:55 +0200 Subject: [PATCH 29/30] deps-dev : bump tsx from 4.16.5 to 4.17.0 (#128) Bumps [tsx](https://github.com/privatenumber/tsx) from 4.16.5 to 4.17.0. - [Release notes](https://github.com/privatenumber/tsx/releases) - [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs) - [Commits](https://github.com/privatenumber/tsx/compare/v4.16.5...v4.17.0) --- updated-dependencies: - dependency-name: tsx dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 290 ++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 142 insertions(+), 150 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6a27ce30..5260987c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,7 +64,7 @@ "stylelint-config-standard-scss": "^13.1.0", "stylelint-scss": "^6.5.0", "tailwindcss": "^3.4.10", - "tsx": "^4.16.5", + "tsx": "^4.17.0", "typescript": "^5.5.4" } }, @@ -381,394 +381,387 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", "cpu": [ "loong64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", "cpu": [ "mips64el" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", "cpu": [ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -3010,42 +3003,42 @@ } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", "dev": true, "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" } }, "node_modules/escalade": { @@ -7229,13 +7222,12 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsx": { - "version": "4.16.5", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.16.5.tgz", - "integrity": "sha512-ArsiAQHEW2iGaqZ8fTA1nX0a+lN5mNTyuGRRO6OW3H/Yno1y9/t1f9YOI1Cfoqz63VAthn++ZYcbDP7jPflc+A==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.17.0.tgz", + "integrity": "sha512-eN4mnDA5UMKDt4YZixo9tBioibaMBpoxBkD+rIPAjVmYERSG0/dWEY1CEFuV89CgASlKL499q8AhmkMnnjtOJg==", "dev": true, - "license": "MIT", "dependencies": { - "esbuild": "~0.21.5", + "esbuild": "~0.23.0", "get-tsconfig": "^4.7.5" }, "bin": { diff --git a/package.json b/package.json index cb645092..83f08aff 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "stylelint-config-standard-scss": "^13.1.0", "stylelint-scss": "^6.5.0", "tailwindcss": "^3.4.10", - "tsx": "^4.16.5", + "tsx": "^4.17.0", "typescript": "^5.5.4" } } From 6cb0461503c0c16331ac348247d088217ac1854a Mon Sep 17 00:00:00 2001 From: Julien Constant <julienconstant190@gmail.com> Date: Mon, 19 Aug 2024 04:06:32 +0200 Subject: [PATCH 30/30] deps: update deps and dev-deps --- package-lock.json | 212 +++++++++++++++++++++++----------------------- package.json | 24 +++--- 2 files changed, 120 insertions(+), 116 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5260987c..9459728b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,14 +12,14 @@ "@auth/prisma-adapter": "^2.4.2", "@hookform/resolvers": "^3.9.0", "@ltd/j-toml": "^1.38.0", - "@mantine/carousel": "^7.12.0", - "@mantine/core": "^7.12.0", - "@mantine/dates": "^7.12.0", - "@mantine/dropzone": "^7.12.0", - "@mantine/form": "^7.12.0", - "@mantine/hooks": "^7.12.0", - "@mantine/modals": "^7.12.0", - "@mantine/notifications": "^7.12.0", + "@mantine/carousel": "^7.12.1", + "@mantine/core": "^7.12.1", + "@mantine/dates": "^7.12.1", + "@mantine/dropzone": "^7.12.1", + "@mantine/form": "^7.12.1", + "@mantine/hooks": "^7.12.1", + "@mantine/modals": "^7.12.1", + "@mantine/notifications": "^7.12.1", "@octokit/rest": "^21.0.2", "@types/unzipper": "^0.10.9", "class-variance-authority": "^0.7.0", @@ -32,13 +32,13 @@ "react-compare-slider": "^3.1.0", "react-dom": "^18", "react-hook-form": "^7.52.2", - "react-icons": "^5.2.1", + "react-icons": "^5.3.0", "react-spinners": "^0.14.1", "sass": "^1.77.8", "server-only": "^0.0.1", "socket.io": "^4.7.5", "socket.io-client": "^4.7.5", - "tailwind-merge": "^2.4.0", + "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "unzipper": "^0.12.1", "uuid": "^10.0.0", @@ -46,7 +46,7 @@ }, "devDependencies": { "@prisma/client": "^5.18.0", - "@types/node": "^20.14.14", + "@types/node": "^20.16.0", "@types/react": "^18.3.3", "@types/react-dom": "^18", "@types/uuid": "^10.0.0", @@ -60,7 +60,7 @@ "postcss-simple-vars": "^7.0.1", "prisma": "^5.18.0", "prisma-json-types-generator": "^3.0.4", - "stylelint": "^16.8.1", + "stylelint": "^16.8.2", "stylelint-config-standard-scss": "^13.1.0", "stylelint-scss": "^6.5.0", "tailwindcss": "^3.4.10", @@ -282,9 +282,9 @@ } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", - "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.1.tgz", + "integrity": "sha512-lSquqZCHxDfuTg/Sk2hiS0mcSFCEBuj49JfzPHJogDBT0mGCyY5A1AQzBWngitrp7i1/HAZpIgzF/VjhOEIJIg==", "dev": true, "funding": [ { @@ -298,16 +298,16 @@ ], "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^2.4.1" + "@csstools/css-tokenizer": "^3.0.1" } }, "node_modules/@csstools/css-tokenizer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", - "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.1.tgz", + "integrity": "sha512-UBqaiu7kU0lfvaP982/o3khfXccVlHPWp0/vwwiIgDF0GmqqqxoiXC/6FCjlS9u92f7CoEz6nXKQnrn1kIAkOw==", "dev": true, "funding": [ { @@ -321,13 +321,13 @@ ], "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=18" } }, "node_modules/@csstools/media-query-list-parser": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz", - "integrity": "sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-3.0.1.tgz", + "integrity": "sha512-HNo8gGD02kHmcbX6PvCoUuOQvn4szyB9ca63vZHKX5A81QytgDG4oxG4IaEfHTlEZSZ6MjPEMWIVU+zF2PZcgw==", "dev": true, "funding": [ { @@ -341,17 +341,17 @@ ], "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1" + "@csstools/css-parser-algorithms": "^3.0.1", + "@csstools/css-tokenizer": "^3.0.1" } }, "node_modules/@csstools/selector-specificity": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.1.1.tgz", - "integrity": "sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-4.0.0.tgz", + "integrity": "sha512-189nelqtPd8++phaHNwYovKZI0FOzH1vQEE3QhHHkNIGrg5fSs9CbYP3RvfEH5geztnIA9Jwq91wyOIwAW5JIQ==", "dev": true, "funding": [ { @@ -363,11 +363,12 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=18" }, "peerDependencies": { - "postcss-selector-parser": "^6.0.13" + "postcss-selector-parser": "^6.1.0" } }, "node_modules/@dual-bundle/import-meta-resolve": { @@ -1001,22 +1002,22 @@ "license": "LGPL-3.0" }, "node_modules/@mantine/carousel": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@mantine/carousel/-/carousel-7.12.0.tgz", - "integrity": "sha512-c+IaeDAHR77E8jbgFWkjN4cA0E2c9fIcvPGZnFNZ7NUFQjuQmaOdNcLqid2FOk+ay889eN+6PGuYmSgW58amCQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@mantine/carousel/-/carousel-7.12.1.tgz", + "integrity": "sha512-dnzd5kJvObjrW3S0N85Pua7sVuB1A47j96FXW/GlV0OnbnMfpZv6aO29Vi9YoqlGzius8rsDzbXmL725xcLPmQ==", "license": "MIT", "peerDependencies": { - "@mantine/core": "7.12.0", - "@mantine/hooks": "7.12.0", + "@mantine/core": "7.12.1", + "@mantine/hooks": "7.12.1", "embla-carousel-react": ">=7.0.0", "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@mantine/core": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.12.0.tgz", - "integrity": "sha512-FxsaIaEnqxV71MBGGsvXXad2q9KYTaIQFVP4TSAZI6xLChklXF/qJTqvabweaoW9BaVQT75b/BnUoJFzPfyAfw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.12.1.tgz", + "integrity": "sha512-PXKIDaT1fpNB77dPQIcdFGM2NRnfmsJSVx3uuBccngBQWMIWI0wPyiO1Y26DK4LQrbrypeb+TS+Zxpgx6RoiCA==", "license": "MIT", "dependencies": { "@floating-ui/react": "^0.26.9", @@ -1027,46 +1028,46 @@ "type-fest": "^4.12.0" }, "peerDependencies": { - "@mantine/hooks": "7.12.0", + "@mantine/hooks": "7.12.1", "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@mantine/dates": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-7.12.0.tgz", - "integrity": "sha512-68oDcDV+FnhQK90J9vFtO872rT303nGwR4DpAQqFAzdNBWxc3h5089/S+rehYryH4Pcwru4t0FqSB4fRvlUtLw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-7.12.1.tgz", + "integrity": "sha512-+Dg5ZGoYPWYRWPY7HagLeW36ayVjKQIkTpdNvgGDwh5YpaFy5cHd6LK6USKUshTsRPuzM3oUKwXIBK8hsigMyA==", "license": "MIT", "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { - "@mantine/core": "7.12.0", - "@mantine/hooks": "7.12.0", + "@mantine/core": "7.12.1", + "@mantine/hooks": "7.12.1", "dayjs": ">=1.0.0", "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@mantine/dropzone": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-7.12.0.tgz", - "integrity": "sha512-emj9D2fCaeLmrBPE15dAZ31tI0hDENMN3Emz1xqMqmR/5xuzsVSTiZRJxIARsHn2Muiva3jLdRLFdW9A8YB3FA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-7.12.1.tgz", + "integrity": "sha512-IuAdCnl6PDtkDnGp4vQlHgxr9z3R7s0685khVKpxy/3f+XfdoswUBBY3X7XyirpDXMIjMD4SLpkIzwuUXgZsag==", "license": "MIT", "dependencies": { "react-dropzone-esm": "15.0.1" }, "peerDependencies": { - "@mantine/core": "7.12.0", - "@mantine/hooks": "7.12.0", + "@mantine/core": "7.12.1", + "@mantine/hooks": "7.12.1", "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@mantine/form": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.12.0.tgz", - "integrity": "sha512-npNHxjis/tOun12EYPYP9cQwJbtFHcGZF1m2yNCcNFVMdkBtTiqH23DdGByXmJRkypYQssSMdQTm3F1zfGsjdQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.12.1.tgz", + "integrity": "sha512-Q+lpgG9N8srlsI0IPnD1V1c2ZaI0xmR3bBEVm+LttSos6Q5zkG49Yy011mc0cXzEKUk2h48j8PLoPHfSEzO03g==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -1077,46 +1078,46 @@ } }, "node_modules/@mantine/hooks": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.12.0.tgz", - "integrity": "sha512-UKMSpQZMdmecZX1PKPoknfUOE9MfDPiZR1myU4wUUKpaZibvvmhYuy8mcOOmYWegapRS3ErKIAc2cNnJ1Dk4RQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.12.1.tgz", + "integrity": "sha512-YPA3qiMHJkWID5+YzakBaLvjHtX3Fg3PdPY49iIb/CaWM9+lrJ+77TOVS7bsY7ZTBHXUfzft1/6Woqt3xSuweA==", "license": "MIT", "peerDependencies": { "react": "^18.2.0" } }, "node_modules/@mantine/modals": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-7.12.0.tgz", - "integrity": "sha512-CXt2nUK0VuWc+cwC1flCeH5FnQYjA8iQfGgZ37wSFv2qxzJFQ61QlRJjdgIG7T+DccUHjqXKkjYohLxXE36EQQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-7.12.1.tgz", + "integrity": "sha512-olS07yDcCFLGylLGaQgBiTnKcRrUZVLKqBFBw5glcmc/wZmJf4SDMgx5mxSwBnsbJOwJ2d3aIYwO/qNTNnluSg==", "license": "MIT", "peerDependencies": { - "@mantine/core": "7.12.0", - "@mantine/hooks": "7.12.0", + "@mantine/core": "7.12.1", + "@mantine/hooks": "7.12.1", "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@mantine/notifications": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.12.0.tgz", - "integrity": "sha512-eW2g66b1K/EUdHD842QnQHWdKWbk1mCJkzDAyxcMGZ2BqU2zzpTUZdexbfDg2BqE/Mj/BGc3B9r2mKHt/6ebBg==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.12.1.tgz", + "integrity": "sha512-YIV2ItCRJzbOjEyXtz5Rjf3qn6kwmcz6CqAGurpd+kecxx6wwNoKuKs6YNlz7tcprFegcH/hCUkW2tVbXHKVBA==", "license": "MIT", "dependencies": { - "@mantine/store": "7.12.0", + "@mantine/store": "7.12.1", "react-transition-group": "4.4.5" }, "peerDependencies": { - "@mantine/core": "7.12.0", - "@mantine/hooks": "7.12.0", + "@mantine/core": "7.12.1", + "@mantine/hooks": "7.12.1", "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@mantine/store": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.12.0.tgz", - "integrity": "sha512-gKOJQVKTxJQbjhG/qlaLiv47ydHgdN+ZC2jFRJHr1jjNeiCqzIT4wX1ofG27c5byPTAwAHvuf+/FLOV3rywUpA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.12.1.tgz", + "integrity": "sha512-zIzYEheEyXchPTNKsm88BJ0CTEZV6ZNwMhMDWHKQE3CzjKLJdKHJdIBcZImRU3Pn4GROZdZdIkQF9HLJ6BjvYw==", "license": "MIT", "peerDependencies": { "react": "^18.2.0" @@ -1616,12 +1617,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", - "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", + "version": "20.16.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.0.tgz", + "integrity": "sha512-vDxceJcoZhIVh67S568bm1UGZO0DX0hpplJZxzeXMKwIPLn190ec5RRxQ69BKhX44SUGIxxgMdDY557lGLKprQ==", "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/prop-types": { @@ -4082,10 +4083,11 @@ } }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -5611,9 +5613,9 @@ } }, "node_modules/postcss-resolve-nested-selector": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.4.tgz", - "integrity": "sha512-R6vHqZWgVnTAPq0C+xjyHfEZqfIYboCBVSy24MjxEDm+tIh1BU4O6o7DP7AA7kHzf136d+Qc5duI4tlpHjixDw==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", + "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==", "dev": true, "license": "MIT" }, @@ -5670,9 +5672,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz", - "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -5889,9 +5891,10 @@ } }, "node_modules/react-icons": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", - "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", + "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==", + "license": "MIT", "peerDependencies": { "react": "*" } @@ -6707,9 +6710,9 @@ } }, "node_modules/stylelint": { - "version": "16.8.1", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.8.1.tgz", - "integrity": "sha512-O8aDyfdODSDNz/B3gW2HQ+8kv8pfhSu7ZR7xskQ93+vI6FhKKGUJMQ03Ydu+w3OvXXE0/u4hWU4hCPNOyld+OA==", + "version": "16.8.2", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.8.2.tgz", + "integrity": "sha512-fInKATippQhcSm7AB+T32GpI+626yohrg33GkFT/5jzliUw5qhlwZq2UQQwgl3HsHrf09oeARi0ZwgY/UWEv9A==", "dev": true, "funding": [ { @@ -6723,10 +6726,10 @@ ], "license": "MIT", "dependencies": { - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/media-query-list-parser": "^2.1.13", - "@csstools/selector-specificity": "^3.1.1", + "@csstools/css-parser-algorithms": "^3.0.0", + "@csstools/css-tokenizer": "^3.0.0", + "@csstools/media-query-list-parser": "^3.0.0", + "@csstools/selector-specificity": "^4.0.0", "@dual-bundle/import-meta-resolve": "^4.1.0", "balanced-match": "^2.0.0", "colord": "^2.9.3", @@ -6741,7 +6744,7 @@ "globby": "^11.1.0", "globjoin": "^0.1.4", "html-tags": "^3.3.1", - "ignore": "^5.3.1", + "ignore": "^5.3.2", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.34.0", @@ -6750,10 +6753,10 @@ "micromatch": "^4.0.7", "normalize-path": "^3.0.0", "picocolors": "^1.0.1", - "postcss": "^8.4.40", - "postcss-resolve-nested-selector": "^0.1.4", + "postcss": "^8.4.41", + "postcss-resolve-nested-selector": "^0.1.6", "postcss-safe-parser": "^7.0.0", - "postcss-selector-parser": "^6.1.1", + "postcss-selector-parser": "^6.1.2", "postcss-value-parser": "^4.2.0", "resolve-from": "^5.0.0", "string-width": "^4.2.3", @@ -7089,9 +7092,9 @@ } }, "node_modules/tailwind-merge": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.4.0.tgz", - "integrity": "sha512-49AwoOQNKdqKPd9CViyH5wJoSKsCDjUlzL8DxuGp3P1FsGY36NJDAa18jLZcaHAUUuTj+JB8IAo8zWgBNvBF7A==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz", + "integrity": "sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==", "license": "MIT", "funding": { "type": "github", @@ -7366,9 +7369,10 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.19.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.6.tgz", + "integrity": "sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==", + "license": "MIT" }, "node_modules/universal-user-agent": { "version": "7.0.2", diff --git a/package.json b/package.json index 83f08aff..b55d519d 100644 --- a/package.json +++ b/package.json @@ -15,14 +15,14 @@ "@auth/prisma-adapter": "^2.4.2", "@hookform/resolvers": "^3.9.0", "@ltd/j-toml": "^1.38.0", - "@mantine/carousel": "^7.12.0", - "@mantine/core": "^7.12.0", - "@mantine/dates": "^7.12.0", - "@mantine/dropzone": "^7.12.0", - "@mantine/form": "^7.12.0", - "@mantine/hooks": "^7.12.0", - "@mantine/modals": "^7.12.0", - "@mantine/notifications": "^7.12.0", + "@mantine/carousel": "^7.12.1", + "@mantine/core": "^7.12.1", + "@mantine/dates": "^7.12.1", + "@mantine/dropzone": "^7.12.1", + "@mantine/form": "^7.12.1", + "@mantine/hooks": "^7.12.1", + "@mantine/modals": "^7.12.1", + "@mantine/notifications": "^7.12.1", "@octokit/rest": "^21.0.2", "@types/unzipper": "^0.10.9", "class-variance-authority": "^0.7.0", @@ -35,13 +35,13 @@ "react-compare-slider": "^3.1.0", "react-dom": "^18", "react-hook-form": "^7.52.2", - "react-icons": "^5.2.1", + "react-icons": "^5.3.0", "react-spinners": "^0.14.1", "sass": "^1.77.8", "server-only": "^0.0.1", "socket.io": "^4.7.5", "socket.io-client": "^4.7.5", - "tailwind-merge": "^2.4.0", + "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "unzipper": "^0.12.1", "uuid": "^10.0.0", @@ -49,7 +49,7 @@ }, "devDependencies": { "@prisma/client": "^5.18.0", - "@types/node": "^20.14.14", + "@types/node": "^20.16.0", "@types/react": "^18.3.3", "@types/react-dom": "^18", "@types/uuid": "^10.0.0", @@ -63,7 +63,7 @@ "postcss-simple-vars": "^7.0.1", "prisma": "^5.18.0", "prisma-json-types-generator": "^3.0.4", - "stylelint": "^16.8.1", + "stylelint": "^16.8.2", "stylelint-config-standard-scss": "^13.1.0", "stylelint-scss": "^6.5.0", "tailwindcss": "^3.4.10",