Skip to content

Commit

Permalink
feat: add progression status to dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
Juknum committed Apr 1, 2024
1 parent 668cabb commit 084f75a
Show file tree
Hide file tree
Showing 10 changed files with 331 additions and 13 deletions.
29 changes: 23 additions & 6 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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")
}
7 changes: 5 additions & 2 deletions src/app/(pages)/(protected)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -23,13 +24,15 @@ const DashboardPage = () => {
<Tabs.List>
<Tabs.Tab value="1">Modpacks</Tabs.Tab>
<Tabs.Tab value="2">Mods</Tabs.Tab>
<Tabs.Tab value="3">Users</Tabs.Tab>
<Tabs.Tab value="3">Progression</Tabs.Tab>
<Tabs.Tab value="4">Users</Tabs.Tab>
</Tabs.List>
</Card>

<Tabs.Panel value="1"><ModpacksPanel /></Tabs.Panel>
<Tabs.Panel value="2"><ModsPanel /></Tabs.Panel>
<Tabs.Panel value="3"><UsersPanel /></Tabs.Panel>
<Tabs.Panel value="3"><ProgressionPanel /></Tabs.Panel>
<Tabs.Panel value="4"><UsersPanel /></Tabs.Panel>
</Tabs>
</RoleGate>
);
Expand Down
3 changes: 3 additions & 0 deletions src/components/dashboard/dashboard.scss
Original file line number Diff line number Diff line change
@@ -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));
Expand Down
90 changes: 90 additions & 0 deletions src/components/dashboard/progression/progression-item.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Popover width={200} position="bottom" withArrow shadow="md" opened={opened}>
<Popover.Target>
<Card
onMouseEnter={open}
onMouseLeave={close}
withBorder
shadow="0"
className="dashboard-mod-progress"
style={{ '--dashboard-mod-progress-count': windowWidth <= BREAKPOINT_MOBILE_LARGE
? 1
: windowWidth <= BREAKPOINT_DESKTOP_MEDIUM
? 2
: windowWidth <= BREAKPOINT_DESKTOP_LARGE
? 3
: 4 }}
>
<Stack gap="0">
<Group justify="space-between">
<Text size="sm" fw={700}>{modVersion.mod.name}</Text>
<Text size="xs" c="dimmed">{modVersion.version}{modVersion.mcVersion}</Text>
</Group>
<Stack gap="sm">
{(Object.keys(modVersion.textures.done) as Resolution[])
.map((res, i) => (
<Stack key={i} gap="0">
<Text size="xs" c="dimmed">
Textures {res}:&nbsp;{modVersion.textures.done[res]}&nbsp;/&nbsp;{modVersion.textures.todo}&nbsp;
{modVersion.textures.todo === modVersion.linkedTextures ? '' : `(linked: ${modVersion.linkedTextures})`}
</Text>

<Progress.Root size="xl" color={gradient.to}>
<Progress.Section value={(modVersion.textures.done[res] / modVersion.textures.todo) * 100}>
<Progress.Label>{(modVersion.textures.done[res] / modVersion.textures.todo * 100).toFixed(2)} %</Progress.Label>
</Progress.Section>
</Progress.Root>
</Stack>
))
}
</Stack>
</Stack>
</Card>
</Popover.Target>
<Popover.Dropdown style={{ pointerEvents: 'none' }}>
<Text size="sm">Per Asset folder</Text>
<Stack mt="sm" gap="xs">
{modVersion.resources.map((resource, index) => {
return (
<div key={index}>
<Stack justify="space-between" gap="0">
<Text size="sm">{resource.assetFolder}</Text>

{(Object.keys(resource.textures.done) as Resolution[])
.map((res, i) => (
<Stack key={i} gap="0">
<Text size="xs" c="dimmed" key={i}>
{res}: {resource.textures.done[res]}/{resource.textures.todo}&nbsp;
{resource.textures.todo === resource.linkedTextures ? '' : `(${resource.linkedTextures})`}
</Text>

<Progress.Root size="xl" color={gradient.to}>
<Progress.Section value={(resource.textures.done[res] / resource.textures.todo) * 100}>
<Progress.Label>{(resource.textures.done[res] / resource.textures.todo * 100).toFixed(2)} %</Progress.Label>
</Progress.Section>
</Progress.Root>
</Stack>
))}

</Stack>
</div>
)
})}
</Stack>
</Popover.Dropdown>
</Popover>
)
}
78 changes: 78 additions & 0 deletions src/components/dashboard/progression/progression-panel.tsx
Original file line number Diff line number Diff line change
@@ -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<ModVersionWithProgression[]>([]);
const [globalProgress, setGlobalProgress] = useState<Progression>(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 (
<Card
shadow="sm"
padding="md"
radius="md"
withBorder
>
<Group justify="space-between" align="flex-start">
<Text size="md" fw={700}>Pack Progression</Text>
<Button
variant='gradient'
gradient={gradient}
className="navbar-icon-fix"
onClick={() => reload()}
>
<TbReload />
</Button>
</Group>

<Stack gap="0" mt="md">
<Text size="sm" fw={700}>Global Progression</Text>
{(Object.keys(globalProgress.textures.done) as Resolution[])
.map((res, i) => (
<Stack key={i} gap="0">
<Text size="xs" c="dimmed">
Textures {res}:&nbsp;{globalProgress.textures.done[res]}&nbsp;/&nbsp;{globalProgress.textures.todo}&nbsp;
{globalProgress.textures.todo === globalProgress.linkedTextures ? '' : `(linked: ${globalProgress.linkedTextures})`}
</Text>

<Progress.Root size="xl" color={gradient.to}>
<Progress.Section value={(globalProgress.textures.done[res] / globalProgress.textures.todo) * 100}>
<Progress.Label>{(globalProgress.textures.done[res] / globalProgress.textures.todo * 100).toFixed(2)} %</Progress.Label>
</Progress.Section>
</Progress.Root>
</Stack>
))
}
</Stack>

<Group gap="md" mt="md">
{resources.map((modVersion, index) => <ProgressionItem key={index} modVersion={modVersion} />)}
</Group>
</Card>
)
}
9 changes: 9 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -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[]) {
Expand Down Expand Up @@ -41,3 +44,9 @@ export function notify(title: string, message: React.ReactNode, color: MantineCo
export function sortByName<T extends { name: string }>(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;
65 changes: 63 additions & 2 deletions src/server/data/mods-version.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -24,6 +25,66 @@ export async function getModVersionsWithModpacks(modId: string): Promise<ModVers
return res;
}

function randInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1) + min);
}

export async function getModsVersionsProgression(): Promise<ModVersionWithProgression[]> {
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<ModVersion[]> {
await canAccess();
const res: ModVersion[] = [];
Expand Down
2 changes: 2 additions & 0 deletions src/server/data/resource.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use server';

import type { Resource } from '@prisma/client';

import { db } from '~/lib/db';
Expand Down
Loading

0 comments on commit 084f75a

Please sign in to comment.