Skip to content
Merged
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
228 changes: 228 additions & 0 deletions src/lib/components/SubmissionStatus/UpdatingDropdown.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
<!-- Usage
// Script
<script lang="ts">
import type { TaskResult } from '$lib/types/task';

import UpdatingDropdown from '$lib/components/SubmissionStatus/UpdatingDropdown.svelte';

interface Props {
taskResult: TaskResult;
isLoggedIn: boolean;
onupdate?: (updatedTask: TaskResult) => void;
}

let { taskResult, isLoggedIn, onupdate = () => {} }: Props = $props();

let updatingDropdown: UpdatingDropdown;
</script>


// Component
<button
type="button"
onclick={() => updatingDropdown.toggle()} // Open / close the dropdown.
>

<UpdatingDropdown bind:this={updatingDropdown} {taskResult} {isLoggedIn} {onupdate} />
-->
<script lang="ts">
import { getStores } from '$app/stores';
import { enhance } from '$app/forms';

import { Dropdown, DropdownUl, DropdownLi, uiHelpers } from 'svelte-5-ui-lib';

import type { TaskResult } from '$lib/types/task';

import InputFieldWrapper from '$lib/components/InputFieldWrapper.svelte';

import { submission_statuses } from '$lib/services/submission_status';
import { errorMessageStore } from '$lib/stores/error_message';

import { SIGNUP_PAGE, LOGIN_PAGE } from '$lib/constants/navbar-links';

interface Props {
taskResult: TaskResult;
isLoggedIn: boolean;
onupdate: (updatedTask: TaskResult) => void; // Ensure to update task result in parent component.
}

let { taskResult, isLoggedIn, onupdate }: Props = $props();

const { page } = getStores();
let activeUrl = $state($page.url.pathname);

let dropdown = uiHelpers();
let dropdownStatus = $state(false);
let closeDropdown = dropdown.close;

$effect(() => {
activeUrl = $page.url.pathname;
dropdownStatus = dropdown.isOpen;
});

export function toggle(): void {
dropdown.toggle();
}

let selectedSubmissionStatus = $state<SubmissionStatus>();
let showForm = $state(false);

function handleClick(submissionStatus: {
innerId: string;
innerName: string;
labelName: string;
}): void {
selectedSubmissionStatus = submissionStatus;
showForm = true;

// Submit after the form is rendered.
setTimeout(() => {
const submitButton = document.querySelector(
'#submissionStatusForm button[type="submit"]',
) as HTMLButtonElement;

if (submitButton) {
// Submit the form via the enhance directive by clicking the button.
submitButton.click();
}
}, 10);
}

type SubmissionStatus = {
innerId: string;
innerName: string;
labelName: string;
};

type EnhanceForSubmit = {
formData: FormData;
action: URL;
cancel: () => void;
};

const FAILED_TO_UPDATE_SUBMISSION_STATUS =
'回答状況の更新に失敗しました。もう一度試してください。';

const handleSubmit = (submissionStatus: SubmissionStatus) => {
return ({ formData, action, cancel }: EnhanceForSubmit) => {
// Cancel the default form submission.
cancel();

if (isSame(submissionStatus, taskResult)) {
console.log('Skipping: Submission status already set to', submissionStatus.labelName);

resetDropdown();
return () => {};
}

// Submit data manually using fetch API.
fetch(action, {
method: 'POST',
body: formData,
headers: {
Accept: 'application/json',
},
})
.then((response) => response.json())
.then(() => {
const updatedTaskResult = updateTaskResult(taskResult, submissionStatus);
onupdate(updatedTaskResult);
})
.catch((error) => {
console.error('Failed to update submission status: ', error);
errorMessageStore.setAndClearAfterTimeout(FAILED_TO_UPDATE_SUBMISSION_STATUS, 10000);
})
.finally(() => {
resetDropdown();
});

// Do not change anything in SvelteKit.
return () => {};
};
};

function isSame(submissionStatus: SubmissionStatus, taskResult: TaskResult): boolean {
return submissionStatus.innerName === taskResult.status_name;
}

function resetDropdown(): void {
closeDropdown();
showForm = false;
}

function updateTaskResult(
taskResult: TaskResult,
submissionStatus: SubmissionStatus,
): TaskResult {
return {
...taskResult,
status_name: submissionStatus.innerName,
status_id: submissionStatus.innerId,
submission_status_label_name: submissionStatus.labelName,
is_ac: submissionStatus.innerName === 'ac',
updated_at: new Date(),
};
}

// TODO: When customizing submission status, implement DB fetching for status options.
const submissionStatusOptions = submission_statuses.map((status) => {
const option = {
innerId: status.id,
innerName: status.status_name,
labelName: status.label_name,
};
return option;
});
</script>

<div class="relative">
<Dropdown
{activeUrl}
{dropdownStatus}
{closeDropdown}
class="absolute w-32 z-20 left-auto right-0 mt-8"
>
<DropdownUl>
{#if isLoggedIn}
{#each submissionStatusOptions as submissionStatus}
<DropdownLi href="javascript:void(0)" onclick={() => handleClick(submissionStatus)}>
{submissionStatus.labelName}
</DropdownLi>
{/each}
{:else}
<DropdownLi href={SIGNUP_PAGE}>アカウント作成</DropdownLi>
<DropdownLi href={LOGIN_PAGE}>ログイン</DropdownLi>
{/if}
</DropdownUl>
</Dropdown>

{#if showForm && selectedSubmissionStatus}
{@render submissionStatusForm(taskResult, selectedSubmissionStatus)}
{/if}
</div>

{#snippet submissionStatusForm(selectedTaskResult: TaskResult, submissionStatus: SubmissionStatus)}
<form
id="submissionStatusForm"
method="POST"
action="?/update"
style="display:none;"
use:enhance={handleSubmit(submissionStatus)}
>
<!-- Task id -->
<InputFieldWrapper
inputFieldType="hidden"
inputFieldName="taskId"
inputValue={selectedTaskResult.task_id}
/>

<!-- Submission status -->
<InputFieldWrapper
inputFieldType="hidden"
inputFieldName="submissionStatus"
inputValue={submissionStatus.innerName}
/>

<button type="submit">Submit</button>
</form>
{/snippet}
62 changes: 43 additions & 19 deletions src/lib/components/TaskTables/TaskTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import type { TaskResults, TaskResult } from '$lib/types/task';
import type { ContestTableProvider } from '$lib/types/contest_table_provider';

import UpdatingModal from '$lib/components/SubmissionStatus/UpdatingModal.svelte';
import TaskTableBodyCell from '$lib/components/TaskTables/TaskTableBodyCell.svelte';

import {
Expand All @@ -31,6 +30,7 @@

let { taskResults, isLoggedIn }: Props = $props();

// Prepare contest table provider based on the active contest type.
let activeContestType = $state<ContestTableProviders>('abcLatest20Rounds');

let provider: ContestTableProvider = $derived(
Expand All @@ -52,20 +52,8 @@
return provider.getContestRoundLabel(contestId);
}

let updatingModal: UpdatingModal | null = null;

// WHY: () => updatingModal.openModal(taskResult) だけだと、updatingModalがnullの可能性があるため。
function openModal(taskResult: TaskResult): void {
if (updatingModal) {
updatingModal.openModal(taskResult);
} else {
console.error('Failed to initialize UpdatingModal component.');
}
}

function getBodyCellClasses(contestId: string, taskIndex: string): string {
const baseClasses =
'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-1 border hover:brightness-125 transition-all';
const baseClasses = 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-1 border';
const backgroundColor = getBackgroundColor(taskTable[contestId][taskIndex]);

return `${baseClasses} ${backgroundColor}`;
Expand All @@ -80,6 +68,43 @@

return '';
}

// Update task results dynamically.
// Computational complexity of preparation table: O(N), where N is the number of task results.
let taskResultsMap = $derived(() => {
return taskResults.reduce((map: Map<string, TaskResult>, taskResult: TaskResult) => {
if (!map.has(taskResult.task_id)) {
map.set(taskResult.task_id, taskResult);
}
return map;
}, new Map<string, TaskResult>());
});

let taskIndicesMap = $derived(() => {
const indices = new Map<string, number>();

taskResults.forEach((task, index) => {
indices.set(task.task_id, index);
});

return indices;
});

function handleUpdateTaskResult(updatedTask: TaskResult): void {
const map = taskResultsMap();

if (map.has(updatedTask.task_id)) {
map.set(updatedTask.task_id, updatedTask);
}

const index = taskIndicesMap().get(updatedTask.task_id);

if (index !== undefined) {
const newTaskResults = [...taskResults];
newTaskResults[index] = updatedTask;
taskResults = newTaskResults;
}
}
</script>

<!-- See: -->
Expand Down Expand Up @@ -117,8 +142,9 @@

{#if taskTableHeaderIds.length}
{#each taskTableHeaderIds as taskTableHeaderId}
<TableHeadCell class="text-center border" scope="col">{taskTableHeaderId}</TableHeadCell
>
<TableHeadCell class="text-center border" scope="col">
{taskTableHeaderId}
</TableHeadCell>
{/each}
{/if}
</TableHead>
Expand All @@ -140,7 +166,7 @@
<TaskTableBodyCell
taskResult={taskTable[contestId][taskTableHeaderId]}
{isLoggedIn}
onClick={() => openModal(taskTable[contestId][taskTableHeaderId])}
onupdate={(updatedTask: TaskResult) => handleUpdateTaskResult(updatedTask)}
/>
{/if}
</TableBodyCell>
Expand All @@ -152,5 +178,3 @@
</Table>
</div>
</div>

<UpdatingModal bind:this={updatingModal} {isLoggedIn} />
Loading
Loading