Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
724ab46
table Veiw
GardKalland May 4, 2025
046f5ab
added back delete
GardKalland May 4, 2025
1c4f848
format
GardKalland May 4, 2025
5e2f924
event button
GardKalland May 4, 2025
82f9824
changed styling✨
GardKalland May 8, 2025
bb11759
sort Active shift
GardKalland May 8, 2025
65a2dc1
New UI✨
GardKalland May 8, 2025
2e2bf43
New UI✨
GardKalland May 13, 2025
073e4bc
format
GardKalland May 14, 2025
5f97f02
format
GardKalland May 19, 2025
989af39
fixed UI
GardKalland May 19, 2025
668918a
fixed new event
GardKalland May 20, 2025
88fc454
Merge branch 'main' of github.com:programmerbar/mono into event_sort
GardKalland May 20, 2025
b8246c6
🧹Cleaning
GardKalland May 20, 2025
8d7d7bb
🧹Cleaning
GardKalland May 20, 2025
1ac803e
added unsaved message
GardKalland May 26, 2025
8277884
styling
GardKalland May 26, 2025
02ae704
removing multiple shift
GardKalland May 28, 2025
f7902c3
UI fix
GardKalland May 28, 2025
76b5af1
Cant join past events
GardKalland May 28, 2025
5cef44a
Mobile layout
GardKalland May 28, 2025
9e11220
Diff redir
GardKalland May 28, 2025
d4b8b47
format
GardKalland May 28, 2025
16f535f
format
GardKalland May 28, 2025
51fbbc1
sort users by name
GardKalland May 30, 2025
97b3c8e
format'
GardKalland May 30, 2025
8bb9ac0
fixed colu
GardKalland May 30, 2025
181ccf8
format
GardKalland May 30, 2025
607b17a
diff mobile veiw
GardKalland May 30, 2025
5aa63e6
removed date
GardKalland May 30, 2025
950dff1
format
GardKalland May 30, 2025
33f0ce1
format
GardKalland May 30, 2025
0f84145
format
GardKalland May 31, 2025
5c79350
lint fix
GardKalland May 31, 2025
d988aed
format
GardKalland May 31, 2025
4b0fe34
Deleted file
GardKalland May 31, 2025
99e20d1
styling
GardKalland May 31, 2025
77b4a52
styling
GardKalland May 31, 2025
14cadb4
fixed time check
GardKalland May 31, 2025
0eb0cd6
styling
GardKalland May 31, 2025
28843e2
format
GardKalland May 31, 2025
ace195d
fixed feedback
GardKalland Jun 4, 2025
71d2cd8
effect to derived
GardKalland Jun 4, 2025
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
222 changes: 222 additions & 0 deletions apps/www/src/lib/components/portal/EventTable.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
<script lang="ts">
import { formatDate } from '$lib/date';
import { SquarePen } from '@lucide/svelte';
import { getUser } from '$lib/context/user.context';
import Button from '../ui/Button.svelte';
import Input from '$lib/components/ui/Input.svelte';

type Event = {
id: string;
name: string;
date: Date;
shifts: {
id: string;
startAt: Date;
endAt: Date;
members: [''];
}[];
};

const { events, outdated } = $props();

let search = $state('');
let activeTab = $state('upcoming');
let user = getUser();

function hasActiveShifts(event: Event) {
const now = new Date();
return event.shifts.some((shift) => {
const startTime = new Date(shift.startAt);
const endTime = new Date(shift.endAt);
return startTime <= now && now < endTime;
});
}

function hasUpcomingShifts(event: Event) {
const now = new Date();
const hasActive = hasActiveShifts(event);
if (hasActive) return false;

return event.shifts.some((shift) => {
const startTime = new Date(shift.startAt);
return now < startTime;
});
}

function hasActiveOrUpcoming(event: Event) {
return hasActiveShifts(event) || hasUpcomingShifts(event);
}

const upcomingEvents = $derived(
[...events, ...outdated.filter((event: Event) => hasActiveOrUpcoming(event))].sort((a, b) => {
if (hasActiveShifts(a)) return -1;
if (hasActiveShifts(b)) return 1;
return 0;
})
);

const pastEvnts = $derived(outdated.filter((event: Event) => !hasActiveOrUpcoming(event)));

const activeEvnts = $derived(activeTab === 'upcoming' ? upcomingEvents : pastEvnts);

let filteredEvents = $derived(
activeEvnts.filter((event: Event) => event.name.toLowerCase().includes(search.toLowerCase()))
);

function countShifts(event: Event) {
return event.shifts.length;
}

function getEventStatus(event: Event) {
if (hasActiveShifts(event)) {
return 'Aktiv';
} else if (hasUpcomingShifts(event)) {
return 'Kommende';
} else {
return 'Ferdig';
}
}

function getStatusClass(status: string) {
switch (status) {
case 'Kommende':
return 'text-blue-500 font-medium';
case 'Aktiv':
return 'text-green-500 font-medium';
case 'Ferdig':
return 'text-gray-500 font-medium';
default:
return '';
}
}

let isMobile = $state(false);

const handleResize = () => {
isMobile = window.innerWidth < 568;
};

$effect(() => {
handleResize();
});
</script>

<svelte:window onresize={handleResize} />
<div class="border-gray overflow-hidden rounded-2xl border-2 bg-background shadow-lg">
<div class="flex flex-wrap gap-2 border-b-2 bg-gray-200 p-2">
<button
class="min-w-[200px] flex-1 cursor-pointer rounded-lg px-6 py-3 font-medium text-gray-500 transition-all duration-200 ease-in-out {activeTab ===
'upcoming'
? 'bg-gray-300 font-semibold'
: 'hover:bg-gray-250 hover:text-blue-500'}"
onclick={() => (activeTab = 'upcoming')}
>
Arrangementer
</button>

<button
class="min-w-[200px] flex-1 cursor-pointer rounded-lg px-6 py-3 font-medium text-gray-500 transition-all duration-200 ease-in-out {activeTab ===
'past'
? 'bg-gray-300 font-semibold'
: 'hover:bg-gray-250 hover:text-blue-500'}"
onclick={() => (activeTab = 'past')}
>
Tidligere arrangementer
</button>
</div>

<div class="flex {isMobile ? 'flex-col' : 'flex-row items-center'} gap-2 p-2">
{#if $user?.role === 'board'}
<a href="arrangementer/ny" class={isMobile ? 'order-first w-full' : 'order-last'}>
<Button type="button" intent="primary" class={isMobile ? 'w-full' : ''}>
Nytt arrangement
</Button>
</a>
{/if}

<Input
type="search"
placeholder="Søk etter arrangementer..."
bind:value={search}
class="w-full flex-1 border-2
{isMobile || !($user?.role === 'board') ? 'pr-4' : 'pr-32'}"
/>
</div>

{#if filteredEvents.length === 0}
<div class="mx-4 my-4 px-8 py-12 text-center font-medium text-gray-500">
Ingen {activeTab === 'upcoming' ? 'kommende' : 'tidligere'} arrangementer å vise.
</div>
{:else}
<div class="overflow-x-auto p-2">
<table class="w-full table-fixed">
{#if isMobile}
<colgroup>
<col class="w-[60%]" />
<col class="w-[40%]" />
</colgroup>
{:else}
<colgroup>
<col class="w-[35%]" />
<col class="w-[20%]" />
<col class="w-[20%]" />
<col class="w-[15%]" />
</colgroup>
{/if}
<thead>
<tr class="border-b-2 border-gray-200">
<th class="p-4 text-left">Arrangement</th>
{#if isMobile}
<th class="p-4 text-left">Status</th>
{:else}
<th class="p-4 text-left">Dato</th>
<th class="p-4 text-left">Antall vakter</th>
<th class="p-4 text-left">Status</th>
<th class="p-4 text-left" aria-label="Handlinger"></th>
{/if}
</tr>
</thead>
<tbody class="whitespace-nowrap break-words border-b border-gray-100">
{#each filteredEvents as event (event.id)}
{@const status = getEventStatus(event)}
<tr class="relative cursor-pointer hover:bg-gray-50">
<td class="overflow-hidden overflow-ellipsis p-4">
<a href="arrangementer/{event.id}" class="absolute inset-0 z-0" aria-hidden="true">
</a>
{event.name}
</td>
{#if isMobile}
<td class="overflow-hidden overflow-ellipsis p-4 {getStatusClass(status)}">
{status}
</td>
{:else}
<td class="overflow-hidden overflow-ellipsis p-4">
{formatDate(event.date)}
</td>
<td class="overflow-hidden overflow-ellipsis p-4">
{countShifts(event)}
</td>
<td class="overflow-hidden overflow-ellipsis p-4 {getStatusClass(status)}">
{status}
</td>
{#if !isMobile}
<td class="p-4">
{#if $user?.role === 'board'}
Comment thread
GardKalland marked this conversation as resolved.
<a
href="arrangementer/{event.id}/edit"
class="relative z-10 inline-flex text-gray-400 opacity-70 transition-all duration-200 ease-in-out hover:text-blue-500 hover:opacity-100"
aria-label="Rediger arrangement"
>
<SquarePen size={18} />
</a>
{/if}
</td>
{/if}
{/if}
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
4 changes: 4 additions & 0 deletions apps/www/src/lib/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ export const time = (date: Dateish) => {
export const normalDate = (date: Dateish) => {
return format(new Date(date), 'dd.MM.yyyy HH:mm');
};

export const ISOStandard = (date: Dateish) => {
return format(new Date(date), "yyyy-MM-dd'T'HH:mm");
};
78 changes: 78 additions & 0 deletions apps/www/src/lib/services/event.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,85 @@ export class EventService {
return event;
}

async updateEvent(id: string, eventData: { name: string; date: Date }) {
const event = await this.#db
.insert(events)
.values({
id,
...eventData
})
.onConflictDoUpdate({
target: events.id,
set: eventData
})
.returning()
.then((rows) => rows[0]);

return event;
}

async updateShift(id: string, shiftData: { eventId: string; startAt: Date; endAt: Date }) {
const shift = await this.#db
.insert(shifts)
.values({
id,
...shiftData
})
.onConflictDoUpdate({
target: shifts.id,
set: shiftData
})
.returning()
.then((rows) => rows[0]);
return shift;
}

async findUpcomingEvents() {
const event = await this.#db.query.events.findMany({
orderBy: (row, { asc }) => [asc(row.date)],
with: {
shifts: {
with: {
members: {
with: {
user: true
}
}
}
}
},
where: (events, { gte }) => gte(events.date, new Date())
});

return event;
}

async findPastEvents() {
const event = await this.#db.query.events.findMany({
orderBy: (row, { desc }) => [desc(row.date)],
with: {
shifts: {
with: {
members: {
with: {
user: true
}
}
}
}
},
where: (events, { lt }) => lt(events.date, new Date())
});

return event;
}

async delete(id: string) {
await this.#db.delete(events).where(eq(events.id, id));
}

async deleteShift(id: string) {
await this.#db.delete(userShifts).where(eq(userShifts.shiftId, id));
await this.#db.delete(shifts).where(eq(shifts.id, id));
}
}
16 changes: 10 additions & 6 deletions apps/www/src/routes/portal/admin/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@
let isModalOpen = $state(false);

let boardMembers = $derived.by(() =>
data.users.filter((user: User) => {
return user.role === 'board' && user.name.toLowerCase().includes(search.toLowerCase());
})
data.users
.filter((user: User) => {
return user.role === 'board' && user.name.toLowerCase().includes(search.toLowerCase());
})
.sort((a, b) => a.name.localeCompare(b.name))
);

let normalMembers = $derived.by(() =>
data.users.filter((user: User) => {
return user.role === 'normal' && user.name.toLowerCase().includes(search.toLowerCase());
})
data.users
.filter((user: User) => {
return user.role === 'normal' && user.name.toLowerCase().includes(search.toLowerCase());
})
.sort((a, b) => a.name.localeCompare(b.name))
);

function closeModal() {
Expand Down
19 changes: 4 additions & 15 deletions apps/www/src/routes/portal/arrangementer/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ locals }) => {
const events = await locals.db.query.events.findMany({
orderBy: (row, { asc }) => [asc(row.date)],
with: {
shifts: true
},
where: (events, { gte }) => gte(events.date, new Date())
});

const outdatedEvents = await locals.db.query.events.findMany({
orderBy: (row, { desc }) => [desc(row.date)],
with: {
shifts: true
},
where: (events, { lt }) => lt(events.date, new Date())
});
const [events, outdatedEvents] = await Promise.all([
locals.eventService.findUpcomingEvents(),
locals.eventService.findPastEvents()
]);

return {
events,
Expand Down
Loading