add assignment completion ui
This commit is contained in:
parent
0d9e2fa71b
commit
9f53245987
3 changed files with 335 additions and 22 deletions
|
|
@ -17,7 +17,7 @@ export const scheduleSummarySchema = z.object({
|
||||||
assignmentCount: z.number().int().nonnegative(),
|
assignmentCount: z.number().int().nonnegative(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const scheduleAssignmentSchema = z.object({
|
const assignmentIdentitySchema = z.object({
|
||||||
id: z.number().int().positive(),
|
id: z.number().int().positive(),
|
||||||
element: z.object({
|
element: z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
|
|
@ -26,7 +26,11 @@ const scheduleAssignmentSchema = z.object({
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const assignmentForDateSchema = scheduleAssignmentSchema.extend({
|
const scheduleAssignmentSchema = assignmentIdentitySchema.extend({
|
||||||
|
completedAt: z.string().datetime({ offset: true }).nullable(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const assignmentForDateSchema = assignmentIdentitySchema.extend({
|
||||||
schedule: z.object({
|
schedule: z.object({
|
||||||
id: z.number().int().positive(),
|
id: z.number().int().positive(),
|
||||||
set: z.object({
|
set: z.object({
|
||||||
|
|
@ -57,6 +61,13 @@ const assignmentsForDateResponseSchema = z.object({
|
||||||
assignments: z.array(assignmentForDateSchema),
|
assignments: z.array(assignmentForDateSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const assignmentCompletionResponseSchema = z.object({
|
||||||
|
assignment: z.object({
|
||||||
|
id: z.number().int().positive(),
|
||||||
|
completedAt: z.string().datetime({ offset: true }).nullable(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
const errorResponseSchema = z.object({
|
const errorResponseSchema = z.object({
|
||||||
error: z.string().min(1),
|
error: z.string().min(1),
|
||||||
})
|
})
|
||||||
|
|
@ -75,6 +86,7 @@ const LIST_ERROR = "We couldn't load your schedules."
|
||||||
const DETAIL_ERROR = "We couldn't load this schedule."
|
const DETAIL_ERROR = "We couldn't load this schedule."
|
||||||
const CREATE_ERROR = "We couldn't create this schedule."
|
const CREATE_ERROR = "We couldn't create this schedule."
|
||||||
const ASSIGNMENTS_ERROR = "We couldn't load today's assignments."
|
const ASSIGNMENTS_ERROR = "We couldn't load today's assignments."
|
||||||
|
const COMPLETION_ERROR = "We couldn't update this assignment."
|
||||||
|
|
||||||
export const useSchedulesStore = defineStore('schedules', () => {
|
export const useSchedulesStore = defineStore('schedules', () => {
|
||||||
const schedules = ref<ScheduleSummary[]>([])
|
const schedules = ref<ScheduleSummary[]>([])
|
||||||
|
|
@ -89,6 +101,8 @@ export const useSchedulesStore = defineStore('schedules', () => {
|
||||||
const assignmentsForDate = ref<AssignmentForDate[]>([])
|
const assignmentsForDate = ref<AssignmentForDate[]>([])
|
||||||
const assignmentsLoading = ref(false)
|
const assignmentsLoading = ref(false)
|
||||||
const assignmentsError = ref<string | null>(null)
|
const assignmentsError = ref<string | null>(null)
|
||||||
|
const assignmentCompletionPendingIds = ref<number[]>([])
|
||||||
|
const assignmentCompletionErrors = ref<Record<number, string>>({})
|
||||||
let activeDetailRequestId = 0
|
let activeDetailRequestId = 0
|
||||||
let assignmentsRequestId = 0
|
let assignmentsRequestId = 0
|
||||||
|
|
||||||
|
|
@ -274,6 +288,84 @@ export const useSchedulesStore = defineStore('schedules', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setAssignmentCompleted(
|
||||||
|
assignmentId: number,
|
||||||
|
completed: boolean,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (assignmentCompletionPendingIds.value.includes(assignmentId)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
assignmentCompletionPendingIds.value = [...assignmentCompletionPendingIds.value, assignmentId]
|
||||||
|
const remainingErrors = { ...assignmentCompletionErrors.value }
|
||||||
|
delete remainingErrors[assignmentId]
|
||||||
|
assignmentCompletionErrors.value = remainingErrors
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/api/assignments/${assignmentId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ completed }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error('assignment completion request failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseBody: unknown = await response.json()
|
||||||
|
const assignment = assignmentCompletionResponseSchema.parse(responseBody).assignment
|
||||||
|
const completionDoesNotMatchRequest = completed
|
||||||
|
? assignment.completedAt === null
|
||||||
|
: assignment.completedAt !== null
|
||||||
|
if (assignment.id !== assignmentId || completionDoesNotMatchRequest) {
|
||||||
|
throw new Error('assignment completion response did not match request')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeSchedule.value !== null) {
|
||||||
|
for (const day of activeSchedule.value.days) {
|
||||||
|
const activeAssignment = day.assignments.find(
|
||||||
|
(candidate) => candidate.id === assignmentId,
|
||||||
|
)
|
||||||
|
if (activeAssignment !== undefined) {
|
||||||
|
activeAssignment.completedAt = assignment.completedAt
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assignment.completedAt !== null) {
|
||||||
|
assignmentsForDate.value = assignmentsForDate.value.filter(
|
||||||
|
(candidate) => candidate.id !== assignmentId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
assignmentCompletionErrors.value = {
|
||||||
|
...assignmentCompletionErrors.value,
|
||||||
|
[assignmentId]: COMPLETION_ERROR,
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
assignmentCompletionPendingIds.value = assignmentCompletionPendingIds.value.filter(
|
||||||
|
(pendingId) => pendingId !== assignmentId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAssignmentCompletionPending(assignmentId: number): boolean {
|
||||||
|
return assignmentCompletionPendingIds.value.includes(assignmentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignmentCompletionError(assignmentId: number): string | null {
|
||||||
|
return assignmentCompletionErrors.value[assignmentId] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
schedules,
|
schedules,
|
||||||
listLoading,
|
listLoading,
|
||||||
|
|
@ -287,9 +379,14 @@ export const useSchedulesStore = defineStore('schedules', () => {
|
||||||
assignmentsForDate,
|
assignmentsForDate,
|
||||||
assignmentsLoading,
|
assignmentsLoading,
|
||||||
assignmentsError,
|
assignmentsError,
|
||||||
|
assignmentCompletionPendingIds,
|
||||||
|
assignmentCompletionErrors,
|
||||||
fetchSchedules,
|
fetchSchedules,
|
||||||
fetchSchedule,
|
fetchSchedule,
|
||||||
createSchedule,
|
createSchedule,
|
||||||
fetchAssignmentsForDate,
|
fetchAssignmentsForDate,
|
||||||
|
setAssignmentCompleted,
|
||||||
|
isAssignmentCompletionPending,
|
||||||
|
assignmentCompletionError,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@ function formatDate(value: string): string {
|
||||||
timeZone: 'UTC',
|
timeZone: 'UTC',
|
||||||
}).format(new Date(`${value}T00:00:00Z`))
|
}).format(new Date(`${value}T00:00:00Z`))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function completeAssignment(assignmentId: number): Promise<void> {
|
||||||
|
await schedulesStore.setAssignmentCompleted(assignmentId, true)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -76,15 +80,19 @@ function formatDate(value: string): string {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul v-else class="today-assignment-list" aria-label="Today's assignments">
|
<ul v-else class="today-assignment-list" aria-label="Today's assignments">
|
||||||
<li v-for="assignment in assignmentsForDate" :key="assignment.id">
|
<li
|
||||||
<RouterLink
|
v-for="assignment in assignmentsForDate"
|
||||||
class="today-assignment-link"
|
:key="assignment.id"
|
||||||
:to="{
|
:data-today-assignment="assignment.id"
|
||||||
name: 'schedule-detail',
|
>
|
||||||
params: { scheduleId: assignment.schedule.id },
|
<article class="today-assignment">
|
||||||
}"
|
<RouterLink
|
||||||
>
|
class="today-assignment-link"
|
||||||
<article class="today-assignment">
|
:to="{
|
||||||
|
name: 'schedule-detail',
|
||||||
|
params: { scheduleId: assignment.schedule.id },
|
||||||
|
}"
|
||||||
|
>
|
||||||
<div class="today-assignment__heading">
|
<div class="today-assignment__heading">
|
||||||
<p>{{ assignment.schedule.set.name }}</p>
|
<p>{{ assignment.schedule.set.name }}</p>
|
||||||
<span class="today-assignment__kind">{{ assignment.element.kind }}</span>
|
<span class="today-assignment__kind">{{ assignment.element.kind }}</span>
|
||||||
|
|
@ -96,8 +104,30 @@ function formatDate(value: string): string {
|
||||||
Open schedule
|
Open schedule
|
||||||
<span aria-hidden="true">→</span>
|
<span aria-hidden="true">→</span>
|
||||||
</span>
|
</span>
|
||||||
</article>
|
</RouterLink>
|
||||||
</RouterLink>
|
<div class="today-assignment__completion">
|
||||||
|
<p
|
||||||
|
v-if="schedulesStore.assignmentCompletionError(assignment.id) !== null"
|
||||||
|
class="assignment-completion-error"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{{ schedulesStore.assignmentCompletionError(assignment.id) }}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="assignment-completion-button"
|
||||||
|
:aria-label="`Mark ${assignment.element.path.join(' / ')} complete`"
|
||||||
|
:disabled="schedulesStore.isAssignmentCompletionPending(assignment.id)"
|
||||||
|
@click="completeAssignment(assignment.id)"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
schedulesStore.isAssignmentCompletionPending(assignment.id)
|
||||||
|
? 'Completing...'
|
||||||
|
: 'Mark complete'
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -313,12 +343,13 @@ function formatDate(value: string): string {
|
||||||
|
|
||||||
.today-assignment-link {
|
.today-assignment-link {
|
||||||
display: block;
|
display: block;
|
||||||
height: 100%;
|
color: inherit;
|
||||||
border-radius: 1rem;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.today-assignment {
|
.today-assignment {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 1.4rem;
|
padding: 1.4rem;
|
||||||
border: 1px solid rgb(24 48 41 / 12%);
|
border: 1px solid rgb(24 48 41 / 12%);
|
||||||
|
|
@ -330,7 +361,7 @@ function formatDate(value: string): string {
|
||||||
transform 160ms ease;
|
transform 160ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.today-assignment-link:hover .today-assignment {
|
.today-assignment:hover {
|
||||||
border-color: rgb(40 92 78 / 35%);
|
border-color: rgb(40 92 78 / 35%);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
@ -377,6 +408,48 @@ function formatDate(value: string): string {
|
||||||
font-weight: 750;
|
font-weight: 750;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.today-assignment__completion {
|
||||||
|
display: grid;
|
||||||
|
justify-items: start;
|
||||||
|
gap: 0.65rem;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 1.15rem;
|
||||||
|
border-top: 1px solid rgb(24 48 41 / 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-completion-button {
|
||||||
|
min-height: 2.5rem;
|
||||||
|
padding: 0.6rem 0.9rem;
|
||||||
|
border: 1px solid rgb(24 58 49 / 28%);
|
||||||
|
border-radius: 0.7rem;
|
||||||
|
color: #fffdf7;
|
||||||
|
background: #285c4e;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-completion-button:hover:not(:disabled) {
|
||||||
|
background: #183a31;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-completion-button:focus-visible {
|
||||||
|
outline: 3px solid rgb(86 127 112 / 34%);
|
||||||
|
outline-offset: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-completion-button:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-completion-error {
|
||||||
|
margin: 0;
|
||||||
|
color: #9b3f32;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.sets-catalog__description {
|
.sets-catalog__description {
|
||||||
max-width: 38rem;
|
max-width: 38rem;
|
||||||
margin: 1.5rem 0 0;
|
margin: 1.5rem 0 0;
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,17 @@ function formatDate(value: string): string {
|
||||||
timeZone: 'UTC',
|
timeZone: 'UTC',
|
||||||
}).format(new Date(`${value}T00:00:00Z`))
|
}).format(new Date(`${value}T00:00:00Z`))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatCompletionTime(value: string): string {
|
||||||
|
return new Intl.DateTimeFormat('en', {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
timeStyle: 'short',
|
||||||
|
}).format(new Date(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setAssignmentCompleted(assignmentId: number, completed: boolean): Promise<void> {
|
||||||
|
await schedulesStore.setAssignmentCompleted(assignmentId, completed)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -98,9 +109,53 @@ function formatDate(value: string): string {
|
||||||
<p v-if="day.assignments.length === 0" class="rest-day">Rest day</p>
|
<p v-if="day.assignments.length === 0" class="rest-day">Rest day</p>
|
||||||
|
|
||||||
<ul v-else class="assignment-list">
|
<ul v-else class="assignment-list">
|
||||||
<li v-for="assignment in day.assignments" :key="assignment.id">
|
<li
|
||||||
<span class="assignment-path">{{ assignment.element.path.join(' / ') }}</span>
|
v-for="assignment in day.assignments"
|
||||||
<span class="assignment-kind">{{ assignment.element.kind }}</span>
|
:key="assignment.id"
|
||||||
|
:class="{ 'is-completed': assignment.completedAt !== null }"
|
||||||
|
:data-schedule-assignment="assignment.id"
|
||||||
|
>
|
||||||
|
<div class="assignment-description">
|
||||||
|
<span class="assignment-path">
|
||||||
|
{{ assignment.element.path.join(' / ') }}
|
||||||
|
</span>
|
||||||
|
<span class="assignment-kind">{{ assignment.element.kind }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="assignment-completion">
|
||||||
|
<p v-if="assignment.completedAt !== null" class="assignment-completion__status">
|
||||||
|
Completed
|
||||||
|
<time :datetime="assignment.completedAt">
|
||||||
|
{{ formatCompletionTime(assignment.completedAt) }}
|
||||||
|
</time>
|
||||||
|
</p>
|
||||||
|
<p v-else class="assignment-completion__status">Not completed</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="assignment-completion-button"
|
||||||
|
:aria-label="
|
||||||
|
assignment.completedAt === null
|
||||||
|
? `Mark ${assignment.element.path.join(' / ')} complete`
|
||||||
|
: `Reopen ${assignment.element.path.join(' / ')}`
|
||||||
|
"
|
||||||
|
:disabled="schedulesStore.isAssignmentCompletionPending(assignment.id)"
|
||||||
|
@click="setAssignmentCompleted(assignment.id, assignment.completedAt === null)"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
schedulesStore.isAssignmentCompletionPending(assignment.id)
|
||||||
|
? 'Saving...'
|
||||||
|
: assignment.completedAt === null
|
||||||
|
? 'Mark complete'
|
||||||
|
: 'Reopen'
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
<p
|
||||||
|
v-if="schedulesStore.assignmentCompletionError(assignment.id) !== null"
|
||||||
|
class="assignment-completion__error"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{{ schedulesStore.assignmentCompletionError(assignment.id) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -223,13 +278,17 @@ h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.assignment-list li {
|
.assignment-list li {
|
||||||
display: flex;
|
display: grid;
|
||||||
align-items: center;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
justify-content: space-between;
|
align-items: start;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assignment-list li.is-completed {
|
||||||
|
background: rgb(220 235 224 / 34%);
|
||||||
|
}
|
||||||
|
|
||||||
.assignment-list li + li {
|
.assignment-list li + li {
|
||||||
border-top: 1px solid rgb(24 48 41 / 10%);
|
border-top: 1px solid rgb(24 48 41 / 10%);
|
||||||
}
|
}
|
||||||
|
|
@ -241,6 +300,14 @@ h1 {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assignment-description {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.assignment-kind {
|
.assignment-kind {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
padding: 0.35rem 0.55rem;
|
padding: 0.35rem 0.55rem;
|
||||||
|
|
@ -253,6 +320,68 @@ h1 {
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assignment-completion {
|
||||||
|
display: grid;
|
||||||
|
justify-items: end;
|
||||||
|
gap: 0.55rem;
|
||||||
|
min-width: 12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-completion__status,
|
||||||
|
.assignment-completion__error {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-completion__status {
|
||||||
|
color: #5e7067;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-completion__status time {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
color: #344e45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-completion__error {
|
||||||
|
max-width: 14rem;
|
||||||
|
color: #9b3f32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-completion-button {
|
||||||
|
min-height: 2.4rem;
|
||||||
|
padding: 0.55rem 0.8rem;
|
||||||
|
border: 1px solid rgb(24 58 49 / 28%);
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
color: #183a31;
|
||||||
|
background: #fffdf7;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-completed .assignment-completion-button {
|
||||||
|
color: #5e7067;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-completion-button:hover:not(:disabled) {
|
||||||
|
border-color: #285c4e;
|
||||||
|
background: #f9f5e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-completion-button:focus-visible {
|
||||||
|
outline: 3px solid rgb(86 127 112 / 34%);
|
||||||
|
outline-offset: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-completion-button:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
.page-state {
|
.page-state {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 10rem;
|
min-height: 10rem;
|
||||||
|
|
@ -302,7 +431,21 @@ h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.assignment-list li {
|
.assignment-list li {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-description {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assignment-completion {
|
||||||
|
justify-items: start;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-completion__status,
|
||||||
|
.assignment-completion__error {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue