Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions ui/src/lib/components/CheckedLabel.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script lang="ts">
import CheckIcon from '$lib/components/icons/CheckIcon.svelte';
import CrossIcon from '$lib/components/icons/CrossIcon.svelte';

let { checked, children } = $props();
</script>

<span class="inline-flex flex-row items-baseline">
{#if checked}
<span class="text-green-400 size-5 inline-block my-auto mr-1">
<CheckIcon />
</span>
{:else}
<span class="text-red-400 size-5 inline-block my-auto mr-1">
<CrossIcon />
</span>
{/if}
{@render children()}
</span>
14 changes: 12 additions & 2 deletions ui/src/lib/components/Navbar.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script lang="ts">
import logoutIcon from '$lib/assets/logout.svg';
import { env } from '$env/dynamic/public';

import AchievementIcon from '$lib/components/icons/AchievementIcon.svelte';

const BACKEND_URL = env.PUBLIC_BACKEND_URL;


Expand All @@ -14,11 +15,20 @@
<div class="flex flex-row items-center">
<img class="size-8" src="/favicon.svg" alt="ZPI Icon">
<span class="ml-2 text-2xl font-extrabold">ZPI</span>
<a class="ml-4 text-lg -my-2 px-3 h-12 inline-flex items-center justify-center hover:bg-amber-600"
href="/achievements">
<span class=" hidden md:inline-block">
Achievements
</span>
<span class="size-7 inline-block md:hidden">
<AchievementIcon />
</span>
</a>
</div>
<!-- Right Content -->
<div class="flex flex-row items-center">
{#if username !== ""}
<span class="text-lg">{username}</span>
<a class="text-lg" href="/profile/{username}">{username}</a>
<a href="{BACKEND_URL}/api/logout">
<img class="size-6 mx-2 invert" src={logoutIcon} alt="Logout" />
</a>
Expand Down
40 changes: 40 additions & 0 deletions ui/src/lib/components/achievements/Achievement.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script lang="ts">
import CheckedLabel from '$lib/components/CheckedLabel.svelte';

let { achievement } = $props();

let goalsUnlocked = [];
for (let i = 0; i < achievement.goals.length; i++) {
goalsUnlocked.push(Math.random() >= 0.5);
}

let imgSrc = 'https://cdn.fastly.steamstatic.com/steamcommunity/public/images/apps/1903340/893a5719f74928a4706ad295b4ab42cf0a2ffacb.jpg';

const achievementUnlocked = () => goalsUnlocked.every(g => g);

const progressBarStyle = `width: ${goalsUnlocked.filter(g => g).length * 100 / goalsUnlocked.length}%`;

</script>

<div class="grid-cols-3 xl:grid-cols-7 grid grid-rows-1 px-6 gap-3 lg:gap-16 mb-6">
{#if achievementUnlocked()}
<img src={imgSrc} alt="achievement" class="col-start-1 rounded-xl">
{:else}
<img src={imgSrc} alt="achievement" class="col-start-1 rounded-xl grayscale">
{/if}
<div class="col-start-2 col-span-6 flex flex-col lg:justify-between">
<span class="font-semibold mb-2 text-2xl lg:text-center">{achievement.name}</span>
<div class="flex flex-col lg:flex-row lg:flex-wrap lg:gap-2 lg:justify-between lg:mr-6 lg:mb-4">
{#each achievement.goals as goal, i}
<div class="grow">
<CheckedLabel checked={goalsUnlocked[i]}>{goal.description}</CheckedLabel>
</div>
{/each}
</div>

<div class="w-full bg-gray-200 rounded-full h-4 dark:bg-gray-700 hidden lg:block lg:mb-6">
<div class="bg-orange-400 h-4 rounded-full" style={progressBarStyle}></div>
</div>

</div>
</div>
39 changes: 39 additions & 0 deletions ui/src/lib/components/achievements/AchievementDisplay.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script lang="ts">
import AchievementServiceGroup from '$lib/components/achievements/AchievementServiceGroup.svelte';
import { createQuery, type CreateQueryResult } from '@tanstack/svelte-query';
import { type AchievementService, getAchievementServices } from '$lib/globalFunctions-Types';
import AchievementEditModal from '$lib/components/achievements/AchievementEditModal.svelte';

let { admin } = $props();

let query: CreateQueryResult<AchievementService[]> = createQuery({
queryKey: [`achievement-services`],
queryFn: getAchievementServices,
retry: false
}
);


let editModal: AchievementEditModal | undefined = $state();

</script>

{#if $query.isSuccess}
<div class="flex flex-col items-center w-full md:w-4/5 mx-auto px-10">
<div class="flex flex-row justify-end w-full mt-10">
<button class="bg-orange-200 hover:bg-orange-300 px-4 py-2 rounded-md font-semibold text-orange-900"
onclick={editModal?.open}
>
Add Service
</button>
</div>
{#each $query.data as service}
<AchievementServiceGroup {service} editAllowed={admin} editModal={editModal} />
{/each}
</div>

{#if admin}
<AchievementEditModal bind:this={editModal} />
{/if}
{/if}

74 changes: 74 additions & 0 deletions ui/src/lib/components/achievements/AchievementEditModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<script lang="ts">
import { createDialog } from 'svelte-headlessui';
import Transition from 'svelte-transition';

const dialog = createDialog({ label: 'Edit Service' });

export function close() {
dialog.close();
}

export function open() {
dialog.open();
}

function save() {
}


</script>
<div class="relative z-10">
<Transition show={$dialog.expanded}>
<Transition
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<button class="fixed inset-0 bg-black/25" aria-label="close" onclick={dialog.close}></button>
</Transition>

<div class="fixed inset-0 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4 text-center">
<Transition
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div
class="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all"
use:dialog.modal
>
<h3 class="text-lg leading-6 font-medium text-gray-900">Edit Service</h3>
<div class="mt-2 flex flex-row justify-center">
// Middle Content
</div>

<div class="mt-4 flex flex-row justify-between">
<button
type="button"
class="inline-flex justify-center rounded-md border-2 border-orange-900 px-4 py-2 text-sm font-medium text-orange-900 hover:bg-orange-200 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-orange-500 focus-visible:ring-offset-2"
onclick={dialog.close}
>
Close
</button>
<button
type="button"
class="inline-flex justify-center rounded-md border border-transparent bg-orange-100 px-4 py-2 text-sm font-medium text-orange-900 hover:bg-orange-200 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-orange-500 focus-visible:ring-offset-2"
onclick={save}
>
Confirm
</button>
</div>
</div>
</Transition>
</div>
</div>
</Transition>
</div>

72 changes: 72 additions & 0 deletions ui/src/lib/components/achievements/AchievementServiceGroup.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<script lang="ts">
import { getAchievementsFromService, toTitleCase } from '$lib/globalFunctions-Types';
import type { Achievement as AchievementPayload } from '$lib/globalFunctions-Types';
import ArrowNoBaseIcon from '$lib/components/icons/ArrowNoBaseIcon.svelte';
import Achievement from '$lib/components/achievements/Achievement.svelte';
import { createQuery, type CreateQueryResult } from '@tanstack/svelte-query';
import { slide } from 'svelte/transition';
import PencilIcon from '$lib/components/icons/PencilIcon.svelte';

let { service, editAllowed, editModal } = $props();

let isOpen = $state(false);

function toggle() {
isOpen = !isOpen;
}

let query: CreateQueryResult<AchievementPayload[]> = createQuery({
queryKey: [`service-${service.id}-achievements`],
queryFn: async () => getAchievementsFromService(service.id),
retry: false
});

function keyClickHandler(event: any, clickFn: Function) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
clickFn();
}
}

function openEdit() {
toggle();
editModal.open();
}
</script>

{#if $query.isSuccess}
<div class="w-full">
<div role="button"
class="flex flex-row justify-between items-center w-full py-4 font-bold text-lg md:text-2xl"
onclick={toggle}
onkeydown={(ev) => keyClickHandler(ev, toggle)}
tabindex="0"
>
<span>
{toTitleCase(service.name)}
{#if editAllowed}
<button class="cursor-pointer rounded-md text-orange-900 bg-orange-200 hover:bg-orange-300 p-1 mx-2"
onclick={openEdit}
onkeydown={(ev) => keyClickHandler(ev, openEdit)}>
<span class="flex justify-center items-center size-4">
<PencilIcon />
</span>
</button>
{/if}
</span>
<span
class="size-10 transition-transform duration-300 {isOpen ? 'rotate-0' : 'rotate-180'}"
>
<ArrowNoBaseIcon />
</span>
</div>

{#if isOpen}
<div transition:slide class="overflow-hidden">
{#each $query.data as achievement }
<Achievement {achievement} />
{/each}
</div>
{/if}
</div>
{/if}
24 changes: 24 additions & 0 deletions ui/src/lib/components/icons/AchievementIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts">
let { fill = 'none', stroke = '#fff' } = $props();
</script>


<svg viewBox="0 0 32.00 32.00"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-96,-192)">
<g transform="matrix(0.785714,0,0,0.785714,-15.2857,40)">
<path
d="M162,196L165.152,198.235L168.65,198.482L170.613,201.387L174.124,203L173.765,206.848L175.3,210L173.765,213.152L174.124,217L170.613,218.613L168.65,221.518L165.152,221.765L162,224L158.848,221.765L155.35,221.518L153.387,218.613L149.876,217L150.235,213.152L148.7,210L150.235,206.848L149.876,203L153.387,201.387L155.35,198.482L158.848,198.235L162,196Z"
fill={fill}></path>
</g>
<g transform="matrix(0.866025,0.5,-0.5,0.866025,121.962,-23.4275)">
<path d="M102,213L102,221L105,219L108,221L108,213" fill={fill}></path>
</g>
<g transform="matrix(-0.866025,0.5,0.5,0.866025,102.031,-23.4275)">
<path d="M102,213L102,221L105,219L108,221L108,213" fill={fill}></path>
</g>
<path
d="M102.835,211.702L98.931,218.464C98.746,218.783 98.753,219.178 98.947,219.492C99.142,219.805 99.493,219.986 99.861,219.962L102.794,219.773C102.794,219.773 104.097,222.407 104.097,222.407C104.26,222.738 104.592,222.952 104.961,222.964C105.329,222.975 105.675,222.783 105.859,222.464L109.789,215.658L110.265,215.996C111.304,216.733 112.696,216.733 113.735,215.996L114.206,215.661L118.134,222.464C118.318,222.783 118.664,222.975 119.032,222.964C119.401,222.952 119.733,222.738 119.896,222.407L121.199,219.773C121.199,219.773 124.132,219.962 124.132,219.962C124.5,219.986 124.851,219.805 125.046,219.492C125.24,219.178 125.247,218.783 125.062,218.464L121.16,211.706C122.01,211.093 122.49,210.066 122.39,208.995C122.355,208.622 122.32,208.24 122.292,207.942C122.275,207.759 122.308,207.576 122.388,207.411C122.388,207.411 122.923,206.313 122.923,206.313C123.326,205.484 123.326,204.516 122.923,203.687C122.923,203.687 122.388,202.589 122.388,202.589C122.308,202.424 122.275,202.241 122.292,202.058C122.32,201.76 122.355,201.378 122.39,201.005C122.509,199.736 121.813,198.531 120.655,198C120.314,197.843 119.966,197.683 119.693,197.558C119.527,197.482 119.385,197.361 119.282,197.209C119.282,197.209 118.599,196.197 118.599,196.197C118.083,195.433 117.244,194.949 116.324,194.884C116.324,194.884 115.106,194.798 115.106,194.798C114.923,194.785 114.748,194.722 114.598,194.616C114.354,194.443 114.041,194.221 113.735,194.004C112.696,193.267 111.304,193.267 110.265,194.004L110.265,194.004C109.959,194.221 109.646,194.443 109.402,194.616C109.252,194.722 109.077,194.785 108.894,194.798C108.894,194.798 107.676,194.884 107.676,194.884C106.756,194.949 105.917,195.433 105.401,196.197C105.401,196.197 104.718,197.209 104.718,197.209C104.615,197.361 104.473,197.482 104.307,197.558C104.034,197.683 103.686,197.843 103.345,198C102.187,198.531 101.491,199.736 101.61,201.005C101.645,201.378 101.68,201.76 101.708,202.058C101.725,202.241 101.692,202.424 101.612,202.589C101.612,202.589 101.077,203.687 101.077,203.687C100.674,204.516 100.674,205.484 101.077,206.313C101.077,206.313 101.612,207.411 101.612,207.411C101.692,207.576 101.725,207.759 101.708,207.942C101.68,208.24 101.645,208.622 101.61,208.995C101.51,210.064 101.989,211.088 102.835,211.702ZM104.599,212.646L101.597,217.846L103.331,217.734C103.733,217.708 104.112,217.927 104.291,218.289C104.291,218.289 105.061,219.846 105.061,219.846L107.787,215.124L107.676,215.116C106.756,215.051 105.917,214.567 105.401,213.803C105.401,213.803 104.718,212.791 104.718,212.791C104.683,212.739 104.643,212.691 104.599,212.646ZM119.397,212.651C119.354,212.694 119.316,212.741 119.282,212.791C119.282,212.791 118.599,213.803 118.599,213.803C118.083,214.567 117.244,215.051 116.324,215.116L116.206,215.124L118.932,219.846L119.702,218.289C119.881,217.927 120.26,217.708 120.662,217.734C120.662,217.734 122.396,217.846 122.396,217.846L119.397,212.651ZM110.559,196.248L111.422,195.636C111.768,195.39 112.232,195.39 112.578,195.636L113.441,196.248C113.89,196.566 114.417,196.754 114.965,196.793L116.183,196.879C116.49,196.901 116.769,197.062 116.941,197.317L117.625,198.328C117.932,198.784 118.359,199.146 118.859,199.376L119.82,199.817C120.206,199.994 120.438,200.396 120.399,200.819L120.3,201.872C120.249,202.42 120.349,202.97 120.59,203.465L121.125,204.562C121.259,204.839 121.259,205.161 121.125,205.438L120.59,206.535C120.349,207.03 120.249,207.58 120.3,208.128L120.399,209.181C120.438,209.604 120.206,210.006 119.82,210.183L118.859,210.624C118.359,210.854 117.932,211.216 117.625,211.672L116.941,212.683C116.769,212.938 116.49,213.099 116.183,213.121L114.965,213.207C114.417,213.246 113.89,213.434 113.441,213.752L112.578,214.364C112.232,214.61 111.768,214.61 111.422,214.364L110.559,213.752C110.11,213.434 109.583,213.246 109.035,213.207L107.817,213.121C107.51,213.099 107.231,212.938 107.059,212.683L106.375,211.672C106.068,211.216 105.641,210.854 105.141,210.624L104.18,210.183C103.794,210.006 103.562,209.604 103.601,209.181L103.7,208.128C103.751,207.58 103.651,207.03 103.41,206.535L102.875,205.438C102.741,205.161 102.741,204.839 102.875,204.562L103.41,203.465C103.651,202.97 103.751,202.42 103.7,201.872L103.601,200.819C103.562,200.396 103.794,199.994 104.18,199.817L105.141,199.376C105.641,199.146 106.068,198.784 106.375,198.328L107.059,197.317C107.231,197.062 107.51,196.901 107.817,196.879L109.035,196.793C109.583,196.754 110.11,196.566 110.559,196.248ZM112,199C108.689,199 106,201.689 106,205C106,208.311 108.689,211 112,211C115.311,211 118,208.311 118,205C118,201.689 115.311,199 112,199ZM112,201C114.208,201 116,202.792 116,205C116,207.208 114.208,209 112,209C109.792,209 108,207.208 108,205C108,202.792 109.792,201 112,201Z"
fill={stroke}></path>
</g>
</svg>
4 changes: 4 additions & 0 deletions ui/src/lib/components/icons/ArrowNoBaseIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="M18.2929 15.2893C18.6834 14.8988 18.6834 14.2656 18.2929 13.8751L13.4007 8.98766C12.6195 8.20726 11.3537 8.20757 10.5729 8.98835L5.68257 13.8787C5.29205 14.2692 5.29205 14.9024 5.68257 15.2929C6.0731 15.6835 6.70626 15.6835 7.09679 15.2929L11.2824 11.1073C11.673 10.7168 12.3061 10.7168 12.6966 11.1073L16.8787 15.2893C17.2692 15.6798 17.9024 15.6798 18.2929 15.2893Z" />
</svg>
6 changes: 6 additions & 0 deletions ui/src/lib/components/icons/CheckIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<g transform="scale(1.3, 1.3) translate(-2.6, -4)">
<path d="M4 12.6111L8.92308 17.5L20 6.5" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</g>
</svg>
6 changes: 6 additions & 0 deletions ui/src/lib/components/icons/CrossIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<g transform="scale(1.9) translate(-5.6,-5.6)">
<path
d="M6.99486 7.00636C6.60433 7.39689 6.60433 8.03005 6.99486 8.42058L10.58 12.0057L6.99486 15.5909C6.60433 15.9814 6.60433 16.6146 6.99486 17.0051C7.38538 17.3956 8.01855 17.3956 8.40907 17.0051L11.9942 13.4199L15.5794 17.0051C15.9699 17.3956 16.6031 17.3956 16.9936 17.0051C17.3841 16.6146 17.3841 15.9814 16.9936 15.5909L13.4084 12.0057L16.9936 8.42059C17.3841 8.03007 17.3841 7.3969 16.9936 7.00638C16.603 6.61585 15.9699 6.61585 15.5794 7.00638L11.9942 10.5915L8.40907 7.00636C8.01855 6.61584 7.38538 6.61584 6.99486 7.00636Z" />
</g>
</svg>
30 changes: 30 additions & 0 deletions ui/src/lib/globalFunctions-Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ export type ProfileData = {
export type CurrentUser = {
id: number;
username: string;
admin: boolean;
};

export type AchievementService = {
id: number;
name: string;
};

export type Achievement = {
id: number;
name: string;
goals: Goal[];
};

export type Goal = {
id: number;
description: string;
sequence: number;
};

export function toTitleCase(str: string) {
Expand Down Expand Up @@ -60,3 +78,15 @@ export async function submitAbout(userId: number, about: string): Promise<Respon
})
});
}

export async function getAchievementServices(): Promise<AchievementService[]> {
return fetch(`${BACKEND_URL}/api/services`, {
credentials: 'include'
}).then((r) => r.json());
}

export async function getAchievementsFromService(serviceId: number): Promise<Achievement[]> {
return fetch(`${BACKEND_URL}/api/services/${serviceId}/achievements`, {
credentials: 'include'
}).then((r) => r.json());
}
2 changes: 1 addition & 1 deletion ui/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
}
);
</script>
<div class="flex flex-col min-h-screen">
<div class="flex flex-col min-h-screen bg-white">
<Navbar username={$query.data?.username || ""} />
{#if $query.isSuccess}
<Profile username={$query.data.username} editAllowed={true} />
Expand Down
Loading