diff --git a/client/src/routes/calendar/+page.svelte b/client/src/routes/calendar/+page.svelte index c2d0a67..c5355eb 100644 --- a/client/src/routes/calendar/+page.svelte +++ b/client/src/routes/calendar/+page.svelte @@ -972,6 +972,7 @@

+
diff --git a/client/src/routes/friends/+page.svelte b/client/src/routes/friends/+page.svelte new file mode 100644 index 0000000..d2d6c57 --- /dev/null +++ b/client/src/routes/friends/+page.svelte @@ -0,0 +1,655 @@ + + +
+
+
+
Friends:
+ {#each friendList as friend} + { + const exists = selectedFriends.includes(friend.id); + const next = exists + ? selectedFriends.filter((id) => id !== friend.id) + : [...selectedFriends, friend.id]; + selectedFriends = next; + if (primaryUser !== 'you' && !next.includes(primaryUser)) { + primaryUser = 'you'; + } + }} + > + {friend.name} + + {/each} +
+
+
Primary:
+ selectedFriends.includes(f.id)) + .map((f) => ({ text: f.name, value: f.id })) + ]} + bind:value={primaryUser} + /> +
+
+
+ +
+ + {#if tab === 'calendar'} + {#if Object.values(stackedMeetings.byDay ?? {}).some((arr) => arr.length > 0)} + {@const latestHour = getLatestEndHourFromBlocks()} + {@const numHours = latestHour - 8 + 1} +
+
+
+
+
+ {#each Array(numHours) as _, i} + {@const hour = i + 8} +
+ {formatHourLabel(hour)} +
+ {/each} +
+ + {#each dayOrder.slice(0, 5) as day} + {@const dayEvents = stackedMeetings.byDay?.[day.key] ?? []} + {@const dayStacks = Math.max(stackedMeetings.maxStacksByDay?.[day.key] ?? 1, 1)} + {@const dayHeight = Math.min(Math.max(120, dayStacks * 52), 190)} +
+
+ {day.label} +
+ +
+ {#each Array(numHours) as _} +
+ {/each} + + {#each dayEvents as item (`${item.ownerId}-${item.meeting.id}`)} + {@const overlapCount = Math.max(item.overlapCount ?? 1, 1)} + {@const heightPct = Math.max((100 - (overlapCount + 1) * stackGapPct) / overlapCount, 0)} + {@const topPct = stackGapPct + item.stackIndex * (heightPct + stackGapPct)} + + {/each} +
+
+ {/each} +
+
+
+ {:else} +
No calendar data available.
+ {/if} + {:else} +
+
+
+
Best times to meet
+
Common free time for selected friends, between 8am–8pm.
+
+
+ + + + + { meetBetweenClassesOnly = !meetBetweenClassesOnly; }}> + Between classes + +
+
+
+ +
+ {#if selectedFriends.length === 0} +
Select at least one friend to see suggestions.
+ {:else if !meetRangeValid} +
Invalid time range.
+ {:else if Object.values(bestMeetTimesByDay ?? {}).some((arr) => arr.length > 0)} +
+ {#each dayOrder.slice(0, 5) as day} + {@const windows = bestMeetTimesByDay?.[day.key] ?? []} + {#if windows.length > 0} +
+
{day.label}
+
+ {#each windows as w (`${day.key}-${w.start}-${w.end}`)} + {}}> + {convertTo12Hour(minutesToHHMM(w.start))} – {convertTo12Hour(minutesToHHMM(w.end))} ({w.duration}m) + + {/each} +
+
+ {/if} + {/each} +
+ {:else} +
No meeting windows match your filters.
+ {/if} +
+ {/if} + +
+ +{#if activeCourse} +
{ if (e.target === e.currentTarget) { activeCourse = undefined; activeMeeting = undefined; activeDay = undefined; } }} + onkeydown={(e) => { if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') { activeCourse = undefined; activeMeeting = undefined; activeDay = undefined; } }} + > +
+
+
+

{activeCourse.title}

+ +
+
+
+
+{/if}