Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
202 changes: 202 additions & 0 deletions app/components/ClassContextSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
<script setup lang="ts">
import { authClient } from '../utils/auth-client'

type ClassOption = {
id: string
name: string
}

const route = useRoute()
const router = useRouter()
const selectedClassId = useCookie<string | null>('selected-class-id', {
sameSite: 'lax',
})
const { data: session } = await authClient.useSession(useFetch)

const { data: classes, status: classesStatus } = await useFetch<ClassOption[]>(
'/api/universal-admin/classes',
{
key: 'universal-admin-classes',
default: () => [],
}
)

function getRouteClassId() {
Comment thread
AidanLoran marked this conversation as resolved.
Outdated
const classQuery = route.query.class
return Array.isArray(classQuery) ? classQuery[0] : classQuery
}

function getCurrentContext() {
if (route.path === '/universal-admin/create-class') {
return 'create-class'
}

if (route.path.startsWith('/admin')) {
const classId = getRouteClassId() || selectedClassId.value
return classId ? `class:${classId}` : 'admin'
}

return 'admin'
}

const selectedContext = ref(getCurrentContext())
const isRedirecting = ref(false)
const canAccessUniversalAdmin = computed(() => session.value?.user?.role === 'admin')
const canManageClasses = computed(
() => session.value?.user?.role === 'admin' || session.value?.user?.role === 'poster'
)

watch(
() => [route.path, route.query.class, selectedClassId.value],
() => {
selectedContext.value = getCurrentContext()
}
)

watch(
[
classesStatus,
() => classes.value.length,
() => route.path,
() => route.query.class,
() => selectedClassId.value,
canAccessUniversalAdmin,
canManageClasses,
],
async () => {
if (!canManageClasses.value || isRedirecting.value) {
return
}

isRedirecting.value = true

try {
if (classesStatus.value !== 'success') {
return
}

if (classes.value.length === 0 && route.path !== '/universal-admin/create-class') {
selectedClassId.value = null
await router.replace('/universal-admin/create-class')
return
}

if (route.path.startsWith('/admin')) {
const classId = getRouteClassId() || selectedClassId.value
const classExists = classes.value.some((classroom) => classroom.id === classId)

if (!classId || !classExists) {
selectedClassId.value = null

if (canAccessUniversalAdmin.value) {
await router.replace('/universal-admin')
return
}

const firstClass = classes.value[0]

if (firstClass) {
selectedClassId.value = firstClass.id
await router.replace({
path: '/admin',
query: { class: firstClass.id },
})
}
}
}
} finally {
isRedirecting.value = false
}
},
{ immediate: true }
)

async function changeContext() {
if (selectedContext.value === 'admin') {
if (!canAccessUniversalAdmin.value) {
selectedContext.value = getCurrentContext()
return
}

selectedClassId.value = null
await router.push('/universal-admin')
return
}

if (selectedContext.value === 'create-class') {
await router.push('/universal-admin/create-class')
return
}

const classId = selectedContext.value.replace(/^class:/, '')
Comment thread
nevins321 marked this conversation as resolved.
Outdated
selectedClassId.value = classId
await router.push({
path: '/admin',
query: { class: classId },
})
}
</script>

<template>
<div class="class-context-select">
<label for="class-context">Managing</label>

<select id="class-context" v-model="selectedContext" @change="changeContext">
<option v-if="canAccessUniversalAdmin" value="admin">Admin</option>

<option v-if="classesStatus === 'pending'" disabled>Loading classes…</option>

<option v-for="classroom in classes" :key="classroom.id" :value="`class:${classroom.id}`">
{{ classroom.name }}
</option>

<option value="create-class">+ Create New Class</option>
</select>
</div>
</template>

<style scoped>
Comment thread
AidanLoran marked this conversation as resolved.
Outdated
.class-context-select {
display: flex;
align-items: center;
gap: 0.75rem;
}

label {
color: #64748b;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}

select {
min-width: 15rem;
padding: 0.7rem 0.9rem;
background: #f8fafc;
border: 1px solid #cbd5e1;
border-radius: 0.65rem;
color: #1e293b;
cursor: pointer;
font: inherit;
font-size: 0.9rem;
font-weight: 700;
}

select:focus {
border-color: #4f46e5;
outline: none;
box-shadow: 0 0 0 3px rgb(79 70 229 / 12%);
}

@media (max-width: 800px) {
.class-context-select {
width: 100%;
}

select {
min-width: 0;
flex: 1;
}
}
</style>
15 changes: 13 additions & 2 deletions app/layouts/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,20 @@ html, body { margin: 0; padding: 0; height: 100%; }
.rh-logout-link:hover { color:#ef4444; }

/* ── Main ── */
.rh-main { flex:1; overflow-y:auto; padding:24px 32px; }
.rh-workspace { min-width:0; flex:1; display:flex; flex-direction:column; overflow:hidden; }
.rh-header { min-height:5.5rem; flex-shrink:0; display:flex; align-items:center; justify-content:space-between; gap:1.5rem; padding:1rem 2rem; background:#ffffff; border-bottom:1px solid #e2e8f0; }
.rh-header-label { margin:0 0 .2rem; color:#94a3b8; font-size:.7rem; font-weight:700; letter-spacing:.1em; text-transform:uppercase; }
.rh-header-title { margin:0; color:#0f172a; font-size:1.35rem; font-weight:600; }
.rh-main { min-height:0; flex:1; overflow-y:auto; padding:24px 32px; }
.rh-tab-content { max-width:56rem; margin:0 auto; }

@media (max-width:800px) {
.rh-admin-wrap { height:auto; min-height:100vh; flex-direction:column; overflow:visible; }
.rh-sidebar { width:100%; }
.rh-workspace { overflow:visible; }
.rh-header { align-items:flex-start; flex-direction:column; }
}

/* ── Spacing helpers ── */
.rh-space-y-3 > * + * { margin-top:12px; }
.rh-space-y-4 > * + * { margin-top:16px; }
Expand Down Expand Up @@ -234,4 +245,4 @@ html, body { margin: 0; padding: 0; height: 100%; }

/* ── Transitions ── */
.rh-fade-enter-active, .rh-fade-leave-active { transition:opacity .25s ease; }
.rh-fade-enter-from, .rh-fade-leave-to { opacity:0; }
.rh-fade-enter-from, .rh-fade-leave-to { opacity:0; }
81 changes: 73 additions & 8 deletions app/layouts/admin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,30 @@ async function logout() {
console.error('Logout failed:', error)
}
}

const selectedClassId = useCookie<string | null>('selected-class-id', {
sameSite: 'lax',
})

const routeClassId = computed(() => {
Comment thread
nevins321 marked this conversation as resolved.
Outdated
const classQuery = route.query.class
return Array.isArray(classQuery) ? classQuery[0] : classQuery
})

watchEffect(() => {
if (routeClassId.value) {
selectedClassId.value = routeClassId.value
}
})

function navigateWithinClass(path: string) {
const classId = routeClassId.value || selectedClassId.value

return navigateTo({
path,
query: classId ? { class: classId } : {},
})
}
</script>

<template>
Expand All @@ -31,11 +55,41 @@ async function logout() {

<p class="rh-nav-label">Admin Tools</p>
Comment thread
nevins321 marked this conversation as resolved.
Outdated
<nav class="rh-nav">
<button @click="navigateTo('/admin')" class="rh-nav-btn" :class="{ 'rh-nav-active': route.path === '/admin' }">Dashboard</button>
<button @click="navigateTo('/admin/builder')" class="rh-nav-btn" :class="{ 'rh-nav-active': route.path === '/admin/builder' }">Form Builder</button>
<button @click="navigateTo('/admin/progress')" class="rh-nav-btn" :class="{ 'rh-nav-active': route.path === '/admin/progress' }">Class Progress</button>
<button @click="navigateTo('/admin/raffle')" class="rh-nav-btn" :class="{ 'rh-nav-active': route.path === '/admin/raffle' }">Raffle System</button>
<button @click="navigateTo('/admin/announcements')" class="rh-nav-btn" :class="{ 'rh-nav-active': route.path === '/admin/announcements' }">Announcements</button>
<button
@click="navigateWithinClass('/admin')"
class="rh-nav-btn"
:class="{ 'rh-nav-active': route.path === '/admin' }"
>
Dashboard
</button>
<button
@click="navigateWithinClass('/admin/builder')"
class="rh-nav-btn"
:class="{ 'rh-nav-active': route.path === '/admin/builder' }"
>
Form Builder
</button>
<button
@click="navigateWithinClass('/admin/progress')"
class="rh-nav-btn"
:class="{ 'rh-nav-active': route.path === '/admin/progress' }"
>
Class Progress
</button>
<button
@click="navigateWithinClass('/admin/raffle')"
class="rh-nav-btn"
:class="{ 'rh-nav-active': route.path === '/admin/raffle' }"
>
Raffle System
</button>
<button
@click="navigateWithinClass('/admin/announcements')"
class="rh-nav-btn"
:class="{ 'rh-nav-active': route.path === '/admin/announcements' }"
>
Announcements
</button>
</nav>
</div>

Expand All @@ -45,12 +99,23 @@ async function logout() {
</div>
</aside>

<div class="rh-main">
<slot />
<div class="rh-workspace">
<header class="rh-header">
<div>
<p class="rh-header-label">Reading Huddle</p>
<h1 class="rh-header-title">Administration</h1>
Comment thread
nevins321 marked this conversation as resolved.
Outdated
</div>

<ClassContextSelect />
</header>

<div class="rh-main">
<slot />
</div>
</div>
</div>
</template>

<style>
@import './admin.css';
</style>
</style>
42 changes: 0 additions & 42 deletions app/layouts/universal-admin.css
Comment thread
AidanLoran marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
Expand Up @@ -173,39 +173,6 @@ body,
font-weight: 600;
}

.portal-context {
display: flex;
align-items: center;
gap: 0.75rem;
}

.portal-context label {
color: #64748b;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}

.portal-context select {
min-width: 15rem;
padding: 0.7rem 0.9rem;
background: #f8fafc;
border: 1px solid #cbd5e1;
border-radius: 0.65rem;
color: #1e293b;
font: inherit;
font-size: 0.9rem;
font-weight: 700;
cursor: pointer;
}

.portal-context select:focus {
border-color: #4f46e5;
outline: none;
box-shadow: 0 0 0 3px rgb(79 70 229 / 12%);
}

.portal-main {
width: 100%;
min-width: 0;
Expand Down Expand Up @@ -247,15 +214,6 @@ body,
flex-direction: column;
}

.portal-context {
width: 100%;
}

.portal-context select {
flex: 1;
min-width: 0;
}

.portal-main {
overflow: visible;
padding: 1rem;
Expand Down
Loading