From 084f75a4b302141b3d431845e43ba2a221fca43c Mon Sep 17 00:00:00 2001 From: Julien Constant Date: Tue, 2 Apr 2024 00:21:43 +0200 Subject: [PATCH] feat: add progression status to dashboard --- prisma/schema.prisma | 29 ++++-- .../(pages)/(protected)/dashboard/page.tsx | 7 +- src/components/dashboard/dashboard.scss | 3 + .../progression/progression-item.tsx | 90 +++++++++++++++++++ .../progression/progression-panel.tsx | 78 ++++++++++++++++ src/lib/utils.ts | 9 ++ src/server/data/mods-version.ts | 65 +++++++++++++- src/server/data/resource.ts | 2 + src/server/data/texture.ts | 33 ++++++- src/types/index.d.ts | 28 +++++- 10 files changed, 331 insertions(+), 13 deletions(-) create mode 100644 src/components/dashboard/progression/progression-item.tsx create mode 100644 src/components/dashboard/progression/progression-panel.tsx diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4094702..04bac75 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,6 +14,17 @@ enum UserRole { BANNED } +enum Resolution { + x32 + x64 +} + +enum Status { + PENDING + ACCEPTED + REJECTED +} + model Account { id String @id @default(cuid()) userId String @map("user_id") @@ -172,10 +183,15 @@ model LinkedTexture { // } model Contribution { - id String @id @default(cuid()) - file String - date DateTime @default(now()) - users User[] + id String @id @default(cuid()) + file String + date DateTime @default(now()) + users User[] + resolution Resolution + status Status + + pollId String + poll Poll @relation(fields: [pollId], references: [id]) Texture Texture? @relation(fields: [textureId], references: [id]) textureId String? @@ -216,8 +232,9 @@ model Poll { upvotes User[] @relation("Upvotes") downvotes User[] @relation("Downvotes") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + Contribution Contribution[] @@map("users_polls") } diff --git a/src/app/(pages)/(protected)/dashboard/page.tsx b/src/app/(pages)/(protected)/dashboard/page.tsx index 254911c..9529188 100644 --- a/src/app/(pages)/(protected)/dashboard/page.tsx +++ b/src/app/(pages)/(protected)/dashboard/page.tsx @@ -6,6 +6,7 @@ import { UserRole } from '@prisma/client'; import { RoleGate } from '~/components/auth/role-gate'; import { ModpacksPanel } from '~/components/dashboard/modpacks/modpacks-panel'; import { ModsPanel } from '~/components/dashboard/mods/mods-panel'; +import { ProgressionPanel } from '~/components/dashboard/progression/progression-panel'; import { UsersPanel } from '~/components/dashboard/users/users-panel'; import { gradient } from '~/lib/utils'; @@ -23,13 +24,15 @@ const DashboardPage = () => { Modpacks Mods - Users + Progression + Users - + + ); diff --git a/src/components/dashboard/dashboard.scss b/src/components/dashboard/dashboard.scss index 9b093da..6d6a99d 100644 --- a/src/components/dashboard/dashboard.scss +++ b/src/components/dashboard/dashboard.scss @@ -1,3 +1,6 @@ +.dashboard-mod-progress { + width: calc((100% - ((var(--dashboard-mod-progress-count) - 1) * var(--mantine-spacing-md))) / var(--dashboard-mod-progress-count)); +} .dashboard-item { width: calc((100% - ((var(--dashboard-item-count) - 1) * var(--mantine-spacing-md))) / var(--dashboard-item-count)); diff --git a/src/components/dashboard/progression/progression-item.tsx b/src/components/dashboard/progression/progression-item.tsx new file mode 100644 index 0000000..c506390 --- /dev/null +++ b/src/components/dashboard/progression/progression-item.tsx @@ -0,0 +1,90 @@ +import { Popover, Card, Stack, Group, Progress, Text } from '@mantine/core' +import { useDisclosure } from '@mantine/hooks'; +import { Resolution } from '@prisma/client'; + +import { useDeviceSize } from '~/hooks/use-device-size'; +import { BREAKPOINT_MOBILE_LARGE, BREAKPOINT_DESKTOP_MEDIUM, BREAKPOINT_DESKTOP_LARGE } from '~/lib/constants' +import { gradient } from '~/lib/utils' +import type { ModVersionWithProgression } from '~/types'; + +export function ProgressionItem({ modVersion }: { modVersion: ModVersionWithProgression }) { + const [opened, { close, open }] = useDisclosure(false); + const [windowWidth, _] = useDeviceSize(); + + return ( + + + + + + {modVersion.mod.name} + {modVersion.version} — {modVersion.mcVersion} + + + {(Object.keys(modVersion.textures.done) as Resolution[]) + .map((res, i) => ( + + + Textures {res}: {modVersion.textures.done[res]} / {modVersion.textures.todo}  + {modVersion.textures.todo === modVersion.linkedTextures ? '' : `(linked: ${modVersion.linkedTextures})`} + + + + + {(modVersion.textures.done[res] / modVersion.textures.todo * 100).toFixed(2)} % + + + + )) + } + + + + + + Per Asset folder + + {modVersion.resources.map((resource, index) => { + return ( +
+ + {resource.assetFolder} + + {(Object.keys(resource.textures.done) as Resolution[]) + .map((res, i) => ( + + + {res}: {resource.textures.done[res]}/{resource.textures.todo}  + {resource.textures.todo === resource.linkedTextures ? '' : `(${resource.linkedTextures})`} + + + + + {(resource.textures.done[res] / resource.textures.todo * 100).toFixed(2)} % + + + + ))} + + +
+ ) + })} +
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/dashboard/progression/progression-panel.tsx b/src/components/dashboard/progression/progression-panel.tsx new file mode 100644 index 0000000..92d00b3 --- /dev/null +++ b/src/components/dashboard/progression/progression-panel.tsx @@ -0,0 +1,78 @@ +import type { Resolution } from '@prisma/client'; + +import { Button, Card, Group, Progress, Text, Stack } from '@mantine/core'; +import { useState } from 'react'; +import { TbReload } from 'react-icons/tb'; + +import { useEffectOnce } from '~/hooks/use-effect-once'; +import { EMPTY_PROGRESSION, gradient } from '~/lib/utils'; +import { getModsVersionsProgression } from '~/server/data/mods-version'; +import { getGlobalProgression } from '~/server/data/texture'; +import type { ModVersionWithProgression, Progression } from '~/types'; + +import { ProgressionItem } from './progression-item'; + +export function ProgressionPanel() { + const [resources, setResources] = useState([]); + const [globalProgress, setGlobalProgress] = useState(EMPTY_PROGRESSION); + + useEffectOnce(() => { + setResources([]); + setGlobalProgress(EMPTY_PROGRESSION); + reload(); + }); + + const reload = () => { + getModsVersionsProgression() + .then((res) => { + setResources(res.sort((a, b) => a.mod.name.localeCompare(b.mod.name))); + }); + getGlobalProgression() + .then(setGlobalProgress) + } + + return ( + + + Pack Progression + + + + + Global Progression + {(Object.keys(globalProgress.textures.done) as Resolution[]) + .map((res, i) => ( + + + Textures {res}: {globalProgress.textures.done[res]} / {globalProgress.textures.todo}  + {globalProgress.textures.todo === globalProgress.linkedTextures ? '' : `(linked: ${globalProgress.linkedTextures})`} + + + + + {(globalProgress.textures.done[res] / globalProgress.textures.todo * 100).toFixed(2)} % + + + + )) + } + + + + {resources.map((modVersion, index) => )} + + + ) +} \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 3bf58cc..055f787 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,8 +1,11 @@ import { MantineColor, MantineGradient } from '@mantine/core' import { showNotification } from '@mantine/notifications' +import { Resolution } from '@prisma/client' import { type ClassValue, clsx } from 'clsx' import { twMerge } from 'tailwind-merge' +import { Progression } from '~/types' + import { NOTIFICATIONS_DURATION_MS } from './constants' export function cn(...inputs: ClassValue[]) { @@ -41,3 +44,9 @@ export function notify(title: string, message: React.ReactNode, color: MantineCo export function sortByName(a: T, b: T) { return a.name.localeCompare(b.name) || 0; } + +export const EMPTY_PROGRESSION_RES = Object.keys(Resolution).reduce((acc, res) => ({ ...acc, [res]: 0 }), {}) as Progression['textures']['done']; +export const EMPTY_PROGRESSION: Progression = { + linkedTextures: 0, + textures: { done: EMPTY_PROGRESSION_RES, todo: 0 }, +} as const; diff --git a/src/server/data/mods-version.ts b/src/server/data/mods-version.ts index 924ee5b..2630d3f 100644 --- a/src/server/data/mods-version.ts +++ b/src/server/data/mods-version.ts @@ -1,10 +1,11 @@ 'use server'; -import type { ModVersion, Modpack } from '@prisma/client'; +import { Resolution, type ModVersion, type Modpack, $Enums } from '@prisma/client'; import { canAccess } from '~/lib/auth'; import { db } from '~/lib/db'; -import type { ModVersionWithModpacks } from '~/types'; +import { EMPTY_PROGRESSION, EMPTY_PROGRESSION_RES } from '~/lib/utils'; +import type { ModVersionWithModpacks, ModVersionWithProgression, Progression } from '~/types'; import { removeModFromModpackVersion } from './modpacks-version'; import { deleteResource } from './resource'; @@ -24,6 +25,66 @@ export async function getModVersionsWithModpacks(modId: string): Promise { + const modVersions = (await db.modVersion.findMany({ include: { mod: true, resources: true } })) + .map((modVer) => ({ + ...modVer, + ...EMPTY_PROGRESSION, + resources: modVer.resources.map((resource) => ({ + ...resource, + ...EMPTY_PROGRESSION, + })) + })) + + for (const modVersion of modVersions) { + for (const resource of modVersion.resources) { + const linkedTextures = await db.linkedTexture.findMany({ where: { resourceId: resource.id }}); + + const textures = await db.texture.findMany({ + where: { + id: { in: linkedTextures.map((lt) => lt.textureId) } + }, + }); + + const contributions = await db.texture.findMany({ + where: { + contributions: { some: {} }, // at least one contribution + id: { in: linkedTextures.map((lt) => lt.textureId) }, + }, + include: { contributions: true }, + }) + // keep contributions only + .then((textures) => textures.map((texture) => texture.contributions).flat()) + // remove multiple contributions on the same resolution for the same texture + .then((contributions) => contributions.filter((c, i, arr) => arr.findIndex((c2) => c2.textureId === c.textureId && c2.resolution === c.resolution) === i)) + // count contributions per resolution + .then((contributions) => { + const output = EMPTY_PROGRESSION_RES; + + for (const contribution of contributions) { + output[contribution.resolution] += 1; + } + + return output; + }) + + modVersion.linkedTextures += linkedTextures.length; + modVersion.textures.todo += textures.length; + (Object.keys(modVersion.textures.done) as Resolution[]).forEach((res) => modVersion.textures.done[res] += contributions[res]); + + resource.linkedTextures = linkedTextures.length; + resource.textures.todo = textures.length; + resource.textures.done = contributions; + } + } + + return modVersions.filter((modVer) => modVer.linkedTextures > 0); // only return mod versions with linked textures +} + export async function addModVersionsFromJAR(jar: FormData): Promise { await canAccess(); const res: ModVersion[] = []; diff --git a/src/server/data/resource.ts b/src/server/data/resource.ts index 8fa114a..745625f 100644 --- a/src/server/data/resource.ts +++ b/src/server/data/resource.ts @@ -1,3 +1,5 @@ +'use server'; + import type { Resource } from '@prisma/client'; import { db } from '~/lib/db'; diff --git a/src/server/data/texture.ts b/src/server/data/texture.ts index fc34f6d..19f7e9a 100644 --- a/src/server/data/texture.ts +++ b/src/server/data/texture.ts @@ -1,6 +1,9 @@ -import { Texture } from '@prisma/client'; +'use server'; + +import { Resolution, Texture } from '@prisma/client'; import { db } from '~/lib/db'; +import type{ Progression } from '~/types'; import { remove } from '../actions/files'; @@ -14,6 +17,34 @@ export async function createTexture({ name, filepath, hash }: { name: string, fi }) } +export async function getGlobalProgression() { + const emptyRes = Object.keys(Resolution).reduce((acc, res) => ({ ...acc, [res]: 0 }), {}) as Progression['textures']['done']; + + const todo = await db.texture.count(); + const linkedTextures = await db.linkedTexture.count(); + + const contributedTextures = await db.texture.findMany({ where: { contributions: { some: {} } }, include: { contributions: true } }) + // keep contributions only + .then((textures) => textures.map((texture) => texture.contributions).flat()) + // remove multiple contributions on the same resolution for the same texture + .then((contributions) => contributions.filter((c, i, arr) => arr.findIndex((c2) => c2.textureId === c.textureId && c2.resolution === c.resolution) === i)) + // count contributions per resolution + .then((contributions) => { + const output = emptyRes; + + for (const contribution of contributions) { + output[contribution.resolution] += 1; + } + + return output; + }) + + return { + linkedTextures, + textures: { done: contributedTextures, todo }, + } +} + export async function findTexture({ hash, }: { diff --git a/src/types/index.d.ts b/src/types/index.d.ts index d452ef7..1554f20 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,13 +1,37 @@ -import type { Modpack, ModpackVersion, ModVersion } from '@prisma/client'; +import type { + LinkedTexture, + Mod, + Modpack, + ModpackVersion, + ModVersion, + Resolution, + Resource, + Texture, +} from '@prisma/client'; export type Prettify = { [K in keyof T]: T[K]; } & {}; export type ModpackVersionWithMods = ModpackVersion & { mods: ModVersion[] }; - export type ModVersionWithModpacks = ModVersion & { modpacks: Modpack[] }; +export type Progression = { + linkedTextures: number; + textures: { + done: { + [key in Resolution]: number; + } + todo: number; + }; +} + +export type ResourceWithProgression = Prettify; +export type ModVersionWithProgression = Prettify; + export type MCModInfoData = MCModInfo[] | { modListVersion: number; modList: MCModInfo[];