The commit adds Markdown rendering capabilities and makes several UI/UX improvements across member-related features including profile display, peer support badges, and navigation structure. Includes: - Added @tailwindcss/typography plugin - New Markdown rendering composable - Simplified member navigation links - Enhanced member profile layout and styling - Added peer support badge component - Improved mobile responsiveness - Removed redundant icons and simplified UI
234 lines
5.9 KiB
Vue
234 lines
5.9 KiB
Vue
<template>
|
|
<nav
|
|
:class="[
|
|
isMobile
|
|
? 'w-full flex flex-col bg-transparent'
|
|
: 'w-64 lg:w-80 backdrop-blur-sm h-screen sticky top-0 flex flex-col bg-ghost-900 border-r border-ghost-700',
|
|
]"
|
|
>
|
|
<!-- Logo/Brand at top (desktop only) -->
|
|
<div v-if="!isMobile" class="p-8 border-b border-ghost-700 bg-primary-500">
|
|
<NuxtLink to="/" class="flex flex-col items-center gap-3 group">
|
|
<span class="text-xl font-bold text-white ethereal-text tracking-wider"
|
|
>Ghost Guild Logo</span
|
|
>
|
|
</NuxtLink>
|
|
</div>
|
|
|
|
<!-- Vertical Navigation -->
|
|
<div
|
|
:class="
|
|
isMobile ? 'flex-1 p-6 overflow-y-auto' : 'flex-1 p-8 overflow-y-auto'
|
|
"
|
|
>
|
|
<ul :class="isMobile ? 'space-y-4' : 'space-y-6'">
|
|
<li v-for="item in navigationItems" :key="item.path">
|
|
<NuxtLink
|
|
:to="item.path"
|
|
class="block group relative"
|
|
@click="handleNavigate"
|
|
>
|
|
<!-- Hover indicator -->
|
|
|
|
<span
|
|
class="text-ghost-200 hover:text-ghost-100 transition-all duration-300 text-lg tracking-wide block py-2 hover:ethereal-text"
|
|
active-class="text-ghost-100 ethereal-text translate-x-2"
|
|
>
|
|
{{ item.label }}
|
|
</span>
|
|
</NuxtLink>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- Color Mode Switcher -->
|
|
<div class="mb-6">
|
|
<UColorModeButton size="md" class="w-full" />
|
|
</div>
|
|
|
|
<!-- Auth section -->
|
|
<div
|
|
:class="
|
|
isMobile
|
|
? 'mt-8 pt-6 border-t border-ghost-800/50'
|
|
: 'mt-12 pt-8 border-t border-ghost-800/50'
|
|
"
|
|
>
|
|
<div v-if="isAuthenticated" class="space-y-4">
|
|
<NuxtLink
|
|
to="/member/dashboard"
|
|
class="block text-ghost-300 hover:text-ghost-100 hover:ethereal-text transition-all duration-300 py-2"
|
|
@click="handleNavigate"
|
|
>
|
|
<span class="block text-sm text-whisper-400 mb-1">{{
|
|
memberData?.name || "Member"
|
|
}}</span>
|
|
Dashboard
|
|
</NuxtLink>
|
|
<button
|
|
@click="handleLogout"
|
|
class="text-ghost-500 hover:text-ghost-300 transition-all duration-300 text-sm"
|
|
>
|
|
Logout
|
|
</button>
|
|
</div>
|
|
<div v-else class="space-y-4">
|
|
<p class="text-ghost-400 text-sm mb-4">
|
|
Enter your email to receive a login link
|
|
</p>
|
|
|
|
<UForm :state="loginForm" @submit="handleLogin">
|
|
<UFormField name="email">
|
|
<UInput
|
|
v-model="loginForm.email"
|
|
type="email"
|
|
size="md"
|
|
placeholder="your.email@example.com"
|
|
class="w-full"
|
|
/>
|
|
</UFormField>
|
|
|
|
<UButton
|
|
type="submit"
|
|
:loading="isLoggingIn"
|
|
:disabled="!isLoginFormValid"
|
|
size="md"
|
|
class="w-full mt-3"
|
|
>
|
|
Send Magic Link
|
|
</UButton>
|
|
</UForm>
|
|
|
|
<div
|
|
v-if="loginSuccess"
|
|
class="p-3 bg-green-900/20 rounded border border-green-800"
|
|
>
|
|
<p class="text-green-300 text-sm">✅ Check your email!</p>
|
|
</div>
|
|
|
|
<div
|
|
v-if="loginError"
|
|
class="p-3 bg-red-900/20 rounded border border-red-800"
|
|
>
|
|
<p class="text-red-300 text-sm">{{ loginError }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { reactive, ref, computed } from "vue";
|
|
|
|
const props = defineProps({
|
|
isMobile: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
});
|
|
|
|
const emit = defineEmits(["navigate"]);
|
|
|
|
const { isAuthenticated, logout, memberData } = useAuth();
|
|
|
|
const handleNavigate = () => {
|
|
if (props.isMobile) {
|
|
emit("navigate");
|
|
}
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
await logout();
|
|
if (props.isMobile) {
|
|
emit("navigate");
|
|
}
|
|
};
|
|
|
|
const publicNavigationItems = [
|
|
{ label: "Home", path: "/", accent: "entry point" },
|
|
{ label: "About", path: "/about", accent: "who we are" },
|
|
{ label: "Events", path: "/events", accent: "gatherings" },
|
|
{ label: "Join", path: "/join", accent: "become one" },
|
|
{ label: "Contact", path: "/contact", accent: "reach out" },
|
|
];
|
|
|
|
const memberNavigationItems = [
|
|
{ label: "Dashboard", path: "/member/dashboard" },
|
|
{ label: "Events", path: "/events" },
|
|
{ label: "Members", path: "/members" },
|
|
{ label: "Resources", path: "/resources" },
|
|
{ label: "Profile", path: "/member/profile" },
|
|
];
|
|
|
|
const navigationItems = computed(() =>
|
|
isAuthenticated.value ? memberNavigationItems : publicNavigationItems,
|
|
);
|
|
|
|
// Login form state
|
|
const loginForm = reactive({
|
|
email: "",
|
|
});
|
|
|
|
const isLoggingIn = ref(false);
|
|
const loginSuccess = ref(false);
|
|
const loginError = ref("");
|
|
|
|
const isLoginFormValid = computed(() => {
|
|
return loginForm.email && loginForm.email.includes("@");
|
|
});
|
|
|
|
const handleLogin = async () => {
|
|
if (isLoggingIn.value) return;
|
|
|
|
isLoggingIn.value = true;
|
|
loginError.value = "";
|
|
loginSuccess.value = false;
|
|
|
|
try {
|
|
const response = await $fetch("/api/auth/login", {
|
|
method: "POST",
|
|
body: {
|
|
email: loginForm.email,
|
|
},
|
|
});
|
|
|
|
if (response.success) {
|
|
loginSuccess.value = true;
|
|
loginError.value = "";
|
|
loginForm.email = "";
|
|
}
|
|
} catch (err) {
|
|
console.error("Login error:", err);
|
|
|
|
if (err.statusCode === 404) {
|
|
loginError.value = "No account found";
|
|
} else if (err.statusCode === 500) {
|
|
loginError.value = "Failed to send email";
|
|
} else {
|
|
loginError.value = "Something went wrong";
|
|
}
|
|
} finally {
|
|
isLoggingIn.value = false;
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
@keyframes float {
|
|
0%,
|
|
100% {
|
|
transform: translateY(0) rotate(0deg);
|
|
}
|
|
50% {
|
|
transform: translateY(-5px) rotate(10deg);
|
|
}
|
|
}
|
|
|
|
.delay-75 {
|
|
animation-delay: 75ms;
|
|
}
|
|
|
|
.delay-150 {
|
|
animation-delay: 150ms;
|
|
}
|
|
</style>
|