✨ Add SPA to the Drive project for further usage
This commit is contained in:
9
DysonNetwork.Drive/Client/src/assets/main.css
Normal file
9
DysonNetwork.Drive/Client/src/assets/main.css
Normal file
@@ -0,0 +1,9 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer theme, base, components, utilities;
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
font-family: 'Nunito Variable', sans-serif;
|
||||
}
|
||||
}
|
115
DysonNetwork.Drive/Client/src/layouts/default.vue
Normal file
115
DysonNetwork.Drive/Client/src/layouts/default.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<n-layout>
|
||||
<n-layout-header class="border-b-1 flex justify-between items-center">
|
||||
<router-link to="/" class="text-lg font-bold">Solar Network Drive</router-link>
|
||||
<div v-if="!hideUserMenu">
|
||||
<n-dropdown
|
||||
v-if="!userStore.isAuthenticated"
|
||||
:options="guestOptions"
|
||||
@select="handleGuestMenuSelect"
|
||||
>
|
||||
<n-button>Account</n-button>
|
||||
</n-dropdown>
|
||||
<n-dropdown v-else :options="userOptions" @select="handleUserMenuSelect" type="primary">
|
||||
<n-button>{{ userStore.user.nick }}</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</n-layout-header>
|
||||
<n-layout-content embedded content-style="padding: 24px;">
|
||||
<router-view />
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, h } from 'vue'
|
||||
import { NLayout, NLayoutHeader, NLayoutContent, NButton, NDropdown, NIcon } from 'naive-ui'
|
||||
import {
|
||||
LogInOutlined,
|
||||
PersonAddAlt1Outlined,
|
||||
LogOutOutlined,
|
||||
PersonOutlineRound,
|
||||
} from '@vicons/material'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// Initialize user state on component mount
|
||||
userStore.initialize()
|
||||
|
||||
const hideUserMenu = computed(() => {
|
||||
return ['captcha', 'spells', 'login', 'create-account'].includes(route.name as string)
|
||||
})
|
||||
|
||||
const guestOptions = [
|
||||
{
|
||||
label: 'Login',
|
||||
key: 'login',
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(LogInOutlined),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Create Account',
|
||||
key: 'create-account',
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(PersonAddAlt1Outlined),
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
const userOptions = computed(() => [
|
||||
{
|
||||
label: 'Profile',
|
||||
key: 'profile',
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(PersonOutlineRound),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Logout',
|
||||
key: 'logout',
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(LogOutOutlined),
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
function handleGuestMenuSelect(key: string) {
|
||||
if (key === 'login') {
|
||||
router.push('/login')
|
||||
} else if (key === 'create-account') {
|
||||
router.push('/create-account')
|
||||
}
|
||||
}
|
||||
|
||||
function handleUserMenuSelect(key: string) {
|
||||
if (key === 'logout') {
|
||||
userStore.logout()
|
||||
router.push('/login')
|
||||
} else if (key === 'profile') {
|
||||
router.push('/accounts/me') // Assuming you have a profile page
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.n-layout-header {
|
||||
padding: 8px 24px;
|
||||
border-color: var(--n-border-color);
|
||||
height: 57px; /* Fixed height */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.n-layout-content {
|
||||
height: calc(100vh - 57px); /* Adjust based on header height */
|
||||
}
|
||||
</style>
|
16
DysonNetwork.Drive/Client/src/main.ts
Normal file
16
DysonNetwork.Drive/Client/src/main.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import '@fontsource-variable/nunito';
|
||||
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import Root from './root.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(Root)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
38
DysonNetwork.Drive/Client/src/root.vue
Normal file
38
DysonNetwork.Drive/Client/src/root.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import LayoutDefault from './layouts/default.vue'
|
||||
|
||||
import { RouterView } from 'vue-router'
|
||||
import { NGlobalStyle, NConfigProvider, NMessageProvider, lightTheme, darkTheme } from 'naive-ui'
|
||||
import { usePreferredDark } from '@vueuse/core'
|
||||
import { useUserStore } from './stores/user'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const themeOverrides = {
|
||||
common: {
|
||||
fontFamily: 'Nunito Variable, v-sans, ui-system, -apple-system, sans-serif',
|
||||
primaryColor: '#7D80BAFF',
|
||||
primaryColorHover: '#9294C5FF',
|
||||
primaryColorPressed: '#575B9DFF',
|
||||
primaryColorSuppl: '#6B6FC1FF',
|
||||
},
|
||||
}
|
||||
|
||||
const isDark = usePreferredDark()
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
onMounted(() => {
|
||||
userStore.fetchUser()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider :theme-overrides="themeOverrides" :theme="isDark ? darkTheme : lightTheme">
|
||||
<n-global-style />
|
||||
<n-message-provider placement="bottom">
|
||||
<layout-default>
|
||||
<router-view />
|
||||
</layout-default>
|
||||
</n-message-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
30
DysonNetwork.Drive/Client/src/router/index.ts
Normal file
30
DysonNetwork.Drive/Client/src/router/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'index',
|
||||
component: () => import('../views/index.vue')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
// Initialize user state if not already initialized
|
||||
if (!userStore.user && localStorage.getItem('authToken')) {
|
||||
await userStore.initialize()
|
||||
}
|
||||
|
||||
if (to.matched.some((record) => record.meta.requiresAuth) && !userStore.isAuthenticated) {
|
||||
next({ name: 'login', query: { redirect: to.fullPath } })
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
3
DysonNetwork.Drive/Client/src/stores/services.ts
Normal file
3
DysonNetwork.Drive/Client/src/stores/services.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useServicesStore = defineStore('services', () => {})
|
59
DysonNetwork.Drive/Client/src/stores/user.ts
Normal file
59
DysonNetwork.Drive/Client/src/stores/user.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// State
|
||||
const user = ref<any>(null)
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// Getters
|
||||
const isAuthenticated = computed(() => !!user.value)
|
||||
|
||||
// Actions
|
||||
async function fetchUser() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await fetch('/api/accounts/me', {
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
// If the token is invalid, clear it and the user state
|
||||
if (response.status === 401) {
|
||||
logout()
|
||||
}
|
||||
throw new Error('Failed to fetch user information.')
|
||||
}
|
||||
|
||||
user.value = await response.json()
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
user.value = null // Clear user data on error
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
user.value = null
|
||||
localStorage.removeItem('authToken')
|
||||
// Optionally, redirect to login page
|
||||
// router.push('/login')
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
await fetchUser()
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
isLoading,
|
||||
error,
|
||||
isAuthenticated,
|
||||
fetchUser,
|
||||
logout,
|
||||
initialize,
|
||||
}
|
||||
})
|
37
DysonNetwork.Drive/Client/src/views/index.vue
Normal file
37
DysonNetwork.Drive/Client/src/views/index.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<section class="h-full relative flex items-center justify-center">
|
||||
<n-card class="max-w-lg" title="About">
|
||||
<p>Welcome to the <b>Solar Drive</b></p>
|
||||
<p>
|
||||
We help you upload, collect, and share files with ease in mind.
|
||||
</p>
|
||||
|
||||
<p class="mt-4 opacity-75 text-xs">
|
||||
<span v-if="version == null">Loading...</span>
|
||||
<span v-else>
|
||||
v{{ version.version }} @
|
||||
{{ version.commit.substring(0, 6) }}
|
||||
{{ version.updatedAt }}
|
||||
</span>
|
||||
</p>
|
||||
</n-card>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NCard } from 'naive-ui'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const version = ref<any>(null)
|
||||
|
||||
async function fetchVersion() {
|
||||
const resp = await fetch('/api/version')
|
||||
version.value = await resp.json()
|
||||
}
|
||||
|
||||
onMounted(() => fetchVersion())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add any specific styles here if needed, though Tailwind should handle most. */
|
||||
</style>
|
Reference in New Issue
Block a user