| # Layout Component Examples |
|
|
| ## Example 1: Dashboard Page |
|
|
| ```vue |
| <template> |
| <AppLayout> |
| <div class="space-y-6"> |
| <h1 class="text-3xl font-bold text-gray-900">Dashboard</h1> |
| |
| <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4"> |
| <!-- Stats Cards --> |
| <div class="rounded-lg bg-white p-6 shadow"> |
| <div class="text-sm text-gray-600">API Keys</div> |
| <div class="text-2xl font-bold text-gray-900">5</div> |
| </div> |
| |
| <div class="rounded-lg bg-white p-6 shadow"> |
| <div class="text-sm text-gray-600">Total Usage</div> |
| <div class="text-2xl font-bold text-gray-900">1,234</div> |
| </div> |
| |
| <div class="rounded-lg bg-white p-6 shadow"> |
| <div class="text-sm text-gray-600">Balance</div> |
| <div class="text-2xl font-bold text-indigo-600">${{ balance }}</div> |
| </div> |
| |
| <div class="rounded-lg bg-white p-6 shadow"> |
| <div class="text-sm text-gray-600">Status</div> |
| <div class="text-2xl font-bold text-green-600">Active</div> |
| </div> |
| </div> |
| |
| <div class="rounded-lg bg-white p-6 shadow"> |
| <h2 class="mb-4 text-xl font-semibold">Recent Activity</h2> |
| <p class="text-gray-600">No recent activity</p> |
| </div> |
| </div> |
| </AppLayout> |
| </template> |
| |
| <script setup lang="ts"> |
| import { computed } from 'vue' |
| import { AppLayout } from '@/components/layout' |
| import { useAuthStore } from '@/stores' |
| |
| const authStore = useAuthStore() |
| const balance = computed(() => authStore.user?.balance.toFixed(2) || '0.00') |
| </script> |
| ``` |
|
|
| --- |
|
|
| ## Example 2: Login Page |
|
|
| ```vue |
| <template> |
| <AuthLayout> |
| <h2 class="mb-6 text-2xl font-bold text-gray-900">Welcome Back</h2> |
| |
| <form @submit.prevent="handleSubmit" class="space-y-4"> |
| <div> |
| <label for="username" class="mb-1 block text-sm font-medium text-gray-700"> |
| Username |
| </label> |
| <input |
| id="username" |
| v-model="form.username" |
| type="text" |
| required |
| class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-indigo-500" |
| placeholder="Enter your username" |
| /> |
| </div> |
| |
| <div> |
| <label for="password" class="mb-1 block text-sm font-medium text-gray-700"> |
| Password |
| </label> |
| <input |
| id="password" |
| v-model="form.password" |
| type="password" |
| required |
| class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-indigo-500" |
| placeholder="Enter your password" |
| /> |
| </div> |
| |
| <button |
| type="submit" |
| :disabled="loading" |
| class="w-full rounded-lg bg-indigo-600 px-4 py-2 text-white transition-colors hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50" |
| > |
| {{ loading ? 'Logging in...' : 'Login' }} |
| </button> |
| </form> |
| |
| <template #footer> |
| <p class="text-gray-600"> |
| Don't have an account? |
| <router-link to="/register" class="font-medium text-indigo-600 hover:underline"> |
| Sign up |
| </router-link> |
| </p> |
| </template> |
| </AuthLayout> |
| </template> |
| |
| <script setup lang="ts"> |
| import { ref } from 'vue' |
| import { useRouter } from 'vue-router' |
| import { AuthLayout } from '@/components/layout' |
| import { useAuthStore, useAppStore } from '@/stores' |
| |
| const router = useRouter() |
| const authStore = useAuthStore() |
| const appStore = useAppStore() |
| |
| const form = ref({ |
| username: '', |
| password: '' |
| }) |
| |
| const loading = ref(false) |
| |
| async function handleSubmit() { |
| loading.value = true |
| try { |
| await authStore.login(form.value) |
| appStore.showSuccess('Login successful!') |
| await router.push('/dashboard') |
| } catch (error) { |
| appStore.showError('Invalid username or password') |
| } finally { |
| loading.value = false |
| } |
| } |
| </script> |
| ``` |
|
|
| --- |
|
|
| ## Example 3: API Keys Page with Custom Header Title |
|
|
| ```vue |
| <template> |
| <AppLayout> |
| <div class="space-y-6"> |
| <!-- Custom page header --> |
| <div class="flex items-center justify-between"> |
| <h1 class="text-3xl font-bold text-gray-900">API Keys</h1> |
| <button |
| @click="showCreateModal = true" |
| class="rounded-lg bg-indigo-600 px-4 py-2 text-white transition-colors hover:bg-indigo-700" |
| > |
| Create New Key |
| </button> |
| </div> |
| |
| <!-- API Keys List --> |
| <div class="overflow-hidden rounded-lg bg-white shadow"> |
| <table class="min-w-full divide-y divide-gray-200"> |
| <thead class="bg-gray-50"> |
| <tr> |
| <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-500">Name</th> |
| <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-500">Key</th> |
| <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-500"> |
| Status |
| </th> |
| <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-500"> |
| Created |
| </th> |
| <th class="px-6 py-3 text-right text-xs font-medium uppercase text-gray-500"> |
| Actions |
| </th> |
| </tr> |
| </thead> |
| <tbody class="divide-y divide-gray-200 bg-white"> |
| <tr v-for="key in apiKeys" :key="key.id"> |
| <td class="whitespace-nowrap px-6 py-4">{{ key.name }}</td> |
| <td class="px-6 py-4 font-mono text-sm">{{ key.key }}</td> |
| <td class="px-6 py-4"> |
| <span |
| class="rounded-full px-2 py-1 text-xs" |
| :class=" |
| key.status === 'active' |
| ? 'bg-green-100 text-green-800' |
| : 'bg-red-100 text-red-800' |
| " |
| > |
| {{ key.status }} |
| </span> |
| </td> |
| <td class="px-6 py-4 text-sm text-gray-500"> |
| {{ new Date(key.created_at).toLocaleDateString() }} |
| </td> |
| <td class="px-6 py-4 text-right"> |
| <button class="text-sm text-red-600 hover:text-red-800">Delete</button> |
| </td> |
| </tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </AppLayout> |
| </template> |
| |
| <script setup lang="ts"> |
| import { ref } from 'vue' |
| import { AppLayout } from '@/components/layout' |
| import type { ApiKey } from '@/types' |
| |
| const showCreateModal = ref(false) |
| const apiKeys = ref<ApiKey[]>([]) |
| |
| // Fetch API keys on mount |
| // fetchApiKeys(); |
| </script> |
| ``` |
|
|
| --- |
|
|
| ## Example 4: Admin Users Page |
|
|
| ```vue |
| <template> |
| <AppLayout> |
| <div class="space-y-6"> |
| <div class="flex items-center justify-between"> |
| <h1 class="text-3xl font-bold text-gray-900">User Management</h1> |
| <button |
| @click="showCreateUser = true" |
| class="rounded-lg bg-indigo-600 px-4 py-2 text-white transition-colors hover:bg-indigo-700" |
| > |
| Create User |
| </button> |
| </div> |
| |
| <!-- Users Table --> |
| <div class="rounded-lg bg-white shadow"> |
| <div class="p-6"> |
| <div class="space-y-4"> |
| <div |
| v-for="user in users" |
| :key="user.id" |
| class="flex items-center justify-between border-b pb-4" |
| > |
| <div> |
| <div class="font-medium text-gray-900">{{ user.username }}</div> |
| <div class="text-sm text-gray-500">{{ user.email }}</div> |
| </div> |
| <div class="flex items-center space-x-4"> |
| <span |
| class="rounded-full px-2 py-1 text-xs" |
| :class=" |
| user.role === 'admin' |
| ? 'bg-purple-100 text-purple-800' |
| : 'bg-blue-100 text-blue-800' |
| " |
| > |
| {{ user.role }} |
| </span> |
| <span class="text-sm font-medium text-gray-700"> |
| ${{ user.balance.toFixed(2) }} |
| </span> |
| <button class="text-sm text-indigo-600 hover:text-indigo-800">Edit</button> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </AppLayout> |
| </template> |
| |
| <script setup lang="ts"> |
| import { ref } from 'vue' |
| import { AppLayout } from '@/components/layout' |
| import type { User } from '@/types' |
| |
| const showCreateUser = ref(false) |
| const users = ref<User[]>([]) |
| |
| // Fetch users on mount |
| // fetchUsers(); |
| </script> |
| ``` |
|
|
| --- |
|
|
| ## Example 5: Profile Page |
|
|
| ```vue |
| <template> |
| <AppLayout> |
| <div class="max-w-2xl space-y-6"> |
| <h1 class="text-3xl font-bold text-gray-900">Profile Settings</h1> |
| |
| <!-- User Info Card --> |
| <div class="space-y-4 rounded-lg bg-white p-6 shadow"> |
| <h2 class="text-xl font-semibold text-gray-900">Account Information</h2> |
| |
| <div class="grid grid-cols-1 gap-4 md:grid-cols-2"> |
| <div> |
| <label class="mb-1 block text-sm font-medium text-gray-700"> Username </label> |
| <div class="rounded-lg bg-gray-50 px-3 py-2 text-gray-900"> |
| {{ user?.username }} |
| </div> |
| </div> |
| |
| <div> |
| <label class="mb-1 block text-sm font-medium text-gray-700"> Email </label> |
| <div class="rounded-lg bg-gray-50 px-3 py-2 text-gray-900"> |
| {{ user?.email }} |
| </div> |
| </div> |
| |
| <div> |
| <label class="mb-1 block text-sm font-medium text-gray-700"> Role </label> |
| <div class="rounded-lg bg-gray-50 px-3 py-2"> |
| <span |
| class="rounded-full px-2 py-1 text-xs" |
| :class=" |
| user?.role === 'admin' |
| ? 'bg-purple-100 text-purple-800' |
| : 'bg-blue-100 text-blue-800' |
| " |
| > |
| {{ user?.role }} |
| </span> |
| </div> |
| </div> |
| |
| <div> |
| <label class="mb-1 block text-sm font-medium text-gray-700"> Balance </label> |
| <div class="rounded-lg bg-gray-50 px-3 py-2 font-semibold text-indigo-600"> |
| ${{ user?.balance.toFixed(2) }} |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <!-- Change Password Card --> |
| <div class="space-y-4 rounded-lg bg-white p-6 shadow"> |
| <h2 class="text-xl font-semibold text-gray-900">Change Password</h2> |
| |
| <form @submit.prevent="handleChangePassword" class="space-y-4"> |
| <div> |
| <label for="old-password" class="mb-1 block text-sm font-medium text-gray-700"> |
| Current Password |
| </label> |
| <input |
| id="old-password" |
| v-model="passwordForm.old_password" |
| type="password" |
| required |
| class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:ring-2 focus:ring-indigo-500" |
| /> |
| </div> |
| |
| <div> |
| <label for="new-password" class="mb-1 block text-sm font-medium text-gray-700"> |
| New Password |
| </label> |
| <input |
| id="new-password" |
| v-model="passwordForm.new_password" |
| type="password" |
| required |
| class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:ring-2 focus:ring-indigo-500" |
| /> |
| </div> |
| |
| <button |
| type="submit" |
| class="rounded-lg bg-indigo-600 px-4 py-2 text-white transition-colors hover:bg-indigo-700" |
| > |
| Update Password |
| </button> |
| </form> |
| </div> |
| </div> |
| </AppLayout> |
| </template> |
| |
| <script setup lang="ts"> |
| import { ref, computed } from 'vue' |
| import { AppLayout } from '@/components/layout' |
| import { useAuthStore, useAppStore } from '@/stores' |
| |
| const authStore = useAuthStore() |
| const appStore = useAppStore() |
| |
| const user = computed(() => authStore.user) |
| |
| const passwordForm = ref({ |
| old_password: '', |
| new_password: '' |
| }) |
| |
| async function handleChangePassword() { |
| try { |
| // await changePasswordAPI(passwordForm.value); |
| appStore.showSuccess('Password updated successfully!') |
| passwordForm.value = { old_password: '', new_password: '' } |
| } catch (error) { |
| appStore.showError('Failed to update password') |
| } |
| } |
| </script> |
| ``` |
|
|
| --- |
|
|
| ## Tips for Using Layouts |
|
|
| 1. **Page Titles**: Set route meta to automatically display page titles in the header |
| 2. **Loading States**: Use `appStore.setLoading(true/false)` for global loading indicators |
| 3. **Toast Notifications**: Use `appStore.showSuccess()`, `appStore.showError()`, etc. |
| 4. **Authentication**: All authenticated pages should use `AppLayout` |
| 5. **Auth Pages**: Login and Register pages should use `AuthLayout` |
| 6. **Sidebar State**: The sidebar state persists across navigation |
| 7. **Mobile First**: All examples are responsive by default using Tailwind's mobile-first approach |
|
|