some UI improvements
This commit is contained in:
parent
ce7f9ac236
commit
d544413cd0
3 changed files with 58 additions and 10 deletions
|
|
@ -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'">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue