some UI improvements

This commit is contained in:
Justin Visser 2026-03-03 17:07:32 +01:00
parent ce7f9ac236
commit d544413cd0
3 changed files with 58 additions and 10 deletions

View file

@ -26,10 +26,18 @@ const teamMembers = ref<{ name: string; student_number: string }[]>(
const newMemberName = ref(""); const newMemberName = ref("");
const newMemberNumber = ref(""); const newMemberNumber = ref("");
function normaliseUrl(url: string): string {
const trimmed = url.trim();
if (trimmed && !/^https?:\/\//i.test(trimmed)) return `https://${trimmed}`;
return trimmed;
}
function handleSave() { function handleSave() {
const updates: { title?: string; content?: string; metadata?: Record<string, unknown> } = {}; const updates: { title?: string; content?: string; metadata?: Record<string, unknown> } = {};
if (title.value !== props.segment.title) updates.title = title.value; if (title.value !== props.segment.title) updates.title = title.value;
if (content.value !== props.segment.content) updates.content = content.value; const savedContent =
props.segment.type === "iframe" ? normaliseUrl(content.value) : content.value;
if (savedContent !== props.segment.content) updates.content = savedContent;
if (props.segment.type === "team") { if (props.segment.type === "team") {
metadata.value = { ...metadata.value, members: teamMembers.value }; metadata.value = { ...metadata.value, members: teamMembers.value };
} }
@ -86,13 +94,17 @@ const assetAcceptMap: Record<string, string> = {
<RichTextEditor v-if="segment.type === 'markdown'" v-model="content" /> <RichTextEditor v-if="segment.type === 'markdown'" v-model="content" />
<!-- Iframe: URL input --> <!-- Iframe: URL input -->
<div v-else-if="segment.type === 'iframe'">
<input <input
v-else-if="segment.type === 'iframe'"
v-model="content" v-model="content"
type="url" type="url"
placeholder="https://..." placeholder="https://..."
class="w-full rounded border border-slate-300 px-3 py-2 text-sm focus:border-primary-400 focus:outline-none dark:border-slate-600 dark:bg-slate-700 dark:text-slate-100" class="w-full rounded border border-slate-300 px-3 py-2 text-sm focus:border-primary-400 focus:outline-none dark:border-slate-600 dark:bg-slate-700 dark:text-slate-100"
/> />
<p class="mt-1 text-xs text-slate-400 dark:text-slate-500">
Most major sites (Google, GitHub, etc.) block embedding. Use dedicated embed URLs where available, e.g. YouTube's <code>youtube.com/embed/</code>.
</p>
</div>
<!-- PDF / Video / Audio: file upload + URL display --> <!-- PDF / Video / Audio: file upload + URL display -->
<div v-else-if="segment.type === 'pdf' || segment.type === 'video' || segment.type === 'audio'"> <div v-else-if="segment.type === 'pdf' || segment.type === 'video' || segment.type === 'audio'">

View file

@ -13,6 +13,8 @@ const { pages, fetchPages } = usePages();
const { isAdmin, authHeaders } = useAdmin(); const { isAdmin, authHeaders } = useAdmin();
const mobileNavOpen = ref(false); const mobileNavOpen = ref(false);
const scrolledDown = ref(false);
const mainEl = ref<HTMLElement | null>(null);
const siteTitle = ref("Untitled Site"); const siteTitle = ref("Untitled Site");
const editingSiteTitle = ref(false); const editingSiteTitle = ref(false);
const siteTitleInput = ref(""); const siteTitleInput = ref("");
@ -63,6 +65,14 @@ watch(pages, (loaded) => {
} }
}); });
function handleScroll(e: Event) {
scrolledDown.value = (e.target as HTMLElement).scrollTop > 200;
}
function scrollToTop() {
mainEl.value?.scrollTo({ top: 0, behavior: "smooth" });
}
onMounted(async () => { onMounted(async () => {
await Promise.all([loadSiteTitle(), fetchPages()]); await Promise.all([loadSiteTitle(), fetchPages()]);
if (pages.value.length > 0 && (route.path === "/" || route.path === "")) { if (pages.value.length > 0 && (route.path === "/" || route.path === "")) {
@ -145,13 +155,30 @@ onMounted(async () => {
</div> </div>
<!-- Scrollable content --> <!-- Scrollable content -->
<main class="flex-1 overflow-y-auto"> <main ref="mainEl" class="flex-1 overflow-y-auto" @scroll="handleScroll">
<div class="mx-auto max-w-4xl px-6 py-10"> <div class="mx-auto max-w-4xl px-6 py-10">
<slot /> <slot />
</div> </div>
</main> </main>
</div> </div>
</div> </div>
<!-- Scroll to top FAB -->
<Teleport to="body">
<Transition name="scroll-top-fade">
<button
v-if="scrolledDown"
class="fixed bottom-20 right-6 z-30 flex items-center gap-2 rounded-full bg-primary-300 px-5 py-3 font-medium text-gray-900 shadow-lg transition-all hover:bg-primary-400 hover:shadow-xl active:scale-95"
aria-label="Scroll to top"
@click="scrollToTop"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
</svg>
Back to top
</button>
</Transition>
</Teleport>
</template> </template>
<style scoped> <style scoped>
@ -172,4 +199,13 @@ onMounted(async () => {
.sidebar-slide-leave-to { .sidebar-slide-leave-to {
transform: translateX(-100%); transform: translateX(-100%);
} }
.scroll-top-fade-enter-active,
.scroll-top-fade-leave-active {
transition: opacity 200ms ease;
}
.scroll-top-fade-enter-from,
.scroll-top-fade-leave-to {
opacity: 0;
}
</style> </style>

View file

@ -161,7 +161,7 @@ async function handleReorder(ids: string[]) {
<!-- Floating "Add section" FAB (admin only) --> <!-- Floating "Add section" FAB (admin only) -->
<Teleport to="body"> <Teleport to="body">
<button <button
v-if="isAdmin && currentPage && !currentPage.is_system" v-if="isAdmin && currentPage && currentPage.slug !== 'team'"
class="fixed right-6 bottom-6 z-30 flex items-center gap-2 rounded-full bg-primary-300 px-5 py-3 font-medium text-gray-900 shadow-lg transition-all hover:bg-primary-400 hover:shadow-xl active:scale-95" class="fixed right-6 bottom-6 z-30 flex items-center gap-2 rounded-full bg-primary-300 px-5 py-3 font-medium text-gray-900 shadow-lg transition-all hover:bg-primary-400 hover:shadow-xl active:scale-95"
title="Add a new section to this page" title="Add a new section to this page"
@click="showAddModal = true" @click="showAddModal = true"