This document provides comprehensive information about Lychee's frontend architecture, built with Vue.js 3, TypeScript, and PrimeVue.
Lychee's frontend is a modern Single Page Application (SPA) built with:
- Vue.js 3 with Composition API and TypeScript
- PrimeVue as the primary UI component library
- Tailwind CSS for styling with custom PrimeUI integration
- Vue Router for client-side routing
- Pinia for state management
- Vite as the build tool and development server
- i18n for internationalization
resources/js/
├── app.ts # Main application entry point
├── components/
│ ├── diagnostics/ # System diagnostic components
│ ├── drawers/ # Side panel/drawer components
│ ├── footers/ # Footer components
│ ├── forms/ # Form components and inputs
│ ├── gallery/ # Photo/album gallery components
│ ├── headers/ # Header and navigation components
│ ├── icons/ # Custom icon components
│ ├── loading/ # Loading state components
│ ├── maintenance/ # System maintenance components
│ ├── modals/ # Modal dialog components
│ ├── settings/ # Settings page components
│ └── statistics/ # Statistics display components
│
├── composables/
│ ├── album/ # Album-related composables
│ ├── contextMenus/ # Context menu logic
│ ├── modalsTriggers/ # Modal state management
│ ├── photo/ # Photo-related composables
│ ├── preview/ # Photo preview functionality
│ ├── search/ # Search functionality
│ └── selections/ # Selection state management
│
├── config/ # Configuration files
├── layouts/ # Photo layout helpers (justified, masonry, etc.)
├── menus/ # Left menu structure definitions
├── router/ # Vue Router configuration
├── services/ # API service layer
├── stores/ # Pinia state stores
├── style/ # Style configurations for PrimeVue
├── utils/ # Utility functions
├── vendor/ # Third-party integrations
└── views/ # Page-level Vue components
├── gallery-panels/ # Gallery-specific views
└── *.vue # Application pages
Lychee uses Vue 3 with the Composition API exclusively, following modern Vue.js best practices while integrating seamlessly with Laravel as the backend API.
- Vue 3.5.18 with Composition API (no Options API)
- TypeScript for type safety and better developer experience
- Pinia for state management with persistence
- Vue Router 4 for client-side routing
- PrimeVue 4 as the primary UI component library
- Vite for build tooling and development server
All Vue components in Lychee use the <script setup> syntax for cleaner, more concise code:
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useLycheeStateStore } from '@/stores/LycheeState'
// Props with TypeScript
const props = defineProps<{
albumId: string
photoId?: string
}>()
// Reactive state
const isLoading = ref(false)
const selectedPhotos = ref<Photo[]>([])
// Computed properties
const hasSelection = computed(() => selectedPhotos.value.length > 0)
// Store usage
const lycheeStore = useLycheeStateStore()
const router = useRouter()
</script>Lychee emphasizes type safety throughout the Vue components:
// Strongly typed props
interface AlbumPanelProps {
albumId: string
photos: App.Http.Resources.Models.PhotoResource[]
config: App.Http.Resources.GalleryConfigs.AlbumConfig
}
const props = defineProps<AlbumPanelProps>()
// Typed reactive references
const user = ref<App.Http.Resources.Models.UserResource | undefined>()
const albums = ref<App.Http.Resources.Models.AlbumResource[]>([])Located in components/, these are reusable UI elements:
- Form inputs and controls
- Modals and dialogs
- Loading states
- Icons and visual elements
Located in views/, these represent full pages:
- Gallery Views: Album, Albums, Favourites, Search, Map, Frame, Flow
- Admin Views: Settings, Users, Permissions, Maintenance, Diagnostics
- System Views: Statistics, Jobs, Profile
State is managed through dedicated stores:
Auth.ts- User authentication and session managementLycheeState.ts- Global application state and configurationLeftMenuState.ts- Left navigation menu stateModalsState.ts- Modal dialog state managementFlowState.ts- Photo flow/timeline stateFavouriteState.ts- Favourites items
import { defineStore } from 'pinia'
export const useFavouriteStore = defineStore("favourite-store", {
state: () => ({
photos: undefined as App.Http.Resources.Models.PhotoResource[] | undefined,
}),
getters: {
getPhotoIds(): string[] {
return this.photos?.map((p) => p.id) ?? [];
},
},
actions: {
addPhoto(photo: App.Http.Resources.Models.PhotoResource) {
if (!this.photos) {
this.photos = [];
}
this.photos.push(photo);
},
},
persist: true,
});// stores/Auth.ts
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null as App.Http.Resources.Models.UserResource | null,
oauthData: undefined as OauthProvider[] | undefined,
}),
actions: {
async getUser(): Promise<App.Http.Resources.Models.UserResource> {
if (this.user === null) {
await AuthService.user().then((response) => {
this.user = response.data
})
}
return this.user as App.Http.Resources.Models.UserResource
},
setUser(user: App.Http.Resources.Models.UserResource | null) {
this.user = user
},
},
})<script setup lang="ts">
import { useAuthStore } from '@/stores/Auth'
import { storeToRefs } from 'pinia'
const auth = useAuthStore()
const { user } = storeToRefs(auth) // Reactive references
// Use store actions
await auth.getUser()
</script>Composables encapsulate reusable functionality:
- Album creation, editing, deletion
- Album navigation and tree operations
- Permission handling
- Photo upload, editing, metadata management
- Photo selection and batch operations
- Preview and slideshow functionality
- Context menus and right-click actions
- Modal state management
- Drag and drop operations
export function useAlbumRefresher(
albumId: Ref<string>,
photoId: Ref<string | undefined>,
auth: AuthStore,
isLoginOpen: Ref<boolean>
) {
const isLoading = ref(false)
const album = ref<AlbumResource | undefined>()
const photos = ref<PhotoResource[]>([])
function loadAlbum(): Promise<void> {
isLoading.value = true
return AlbumService.get(albumId.value).then((data) => {
album.value = data.data.resource
photos.value = data.data.photos
isLoading.value = false
})
}
return {
isLoading,
album,
photos,
loadAlbum,
}
}<script setup lang="ts">
const props = defineProps<{ albumId: string }>()
const albumId = ref(props.albumId)
const { isLoading, album, photos, loadAlbum } = useAlbumRefresher(
albumId,
photoId,
auth,
isLoginOpen
)
// Load on mount
onMounted(() => loadAlbum())
</script>// Very simplied!
export function usePhotoSelection() {
const selectedPhotos = ref<Photo[]>([])
function selectPhoto(photo: Photo) {
// Selection logic
}
function clearSelection() {
selectedPhotos.value = []
}
return {
selectedPhotos: readonly(selectedPhotos),
selectPhoto,
clearSelection
}
}Services in services/ handle API communication:
album-service.ts- Album CRUD operationsphoto-service.ts- Photo management and uploadsettings-service.ts- Application configurationuser-service.ts- User management and authentication
export class AlbumService {
create(data: CreateAlbumData): Promise<AxiosResponse<Album>> {
return axios.post('/api/albums', data)
}
getAlbum(id: string): Promise<AxiosResponse<Album>> {
return axios.get(`/api/albums/${id}`)
}
}Vue Router handles client-side navigation with:
- Gallery Routes:
/gallery/*- Photo and album browsing - Admin Routes:
/settings,/users,/maintenance- Administration - Utility Routes:
/search,/map,/frame- Special views
Components handle route parameters reactively:
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// Reactive route params
const albumId = computed(() => route.params.albumId as string)
const photoId = computed(() => route.params.photoId as string | undefined)
// Navigation
function goBack() {
router.push({ name: 'gallery' })
}
function openPhoto(photo: Photo) {
router.push({
name: 'album',
params: { albumId: albumId.value, photoId: photo.id }
})
}
</script>Routes use lazy loading for better performance:
const Settings = () => import('@/views/Settings.vue')Lychee uses defineEmits for component communication:
<script setup lang="ts">
interface PhotoThumbEvents {
clicked: [index: number, event: MouseEvent]
selected: [photo: Photo]
}
const emit = defineEmits<PhotoThumbEvents>()
function handleClick(event: MouseEvent) {
emit('clicked', props.index, event)
}
</script>Global keyboard handling using VueUse:
import { onKeyStroke } from '@vueuse/core'
import { shouldIgnoreKeystroke } from '@/utils/keybindings-utils'
// Global shortcuts
onKeyStroke('f', () => !shouldIgnoreKeystroke() && toggleFullscreen())
onKeyStroke('ArrowLeft', () => !shouldIgnoreKeystroke() && previousPhoto())
onKeyStroke('Escape', () => !shouldIgnoreKeystroke() && goBack())Lychee leverages PrimeVue components throughout:
<template>
<Button @click="handleSubmit" :loading="isSubmitting">
Submit
</Button>
<Dialog v-model:visible="isDialogOpen" :modal="true">
<template #header>
<h3>Photo Details</h3>
</template>
<!-- Dialog content -->
</Dialog>
<Toast />
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Dialog from 'primevue/dialog'
import Toast from 'primevue/toast'
import { useToast } from 'primevue/usetoast'
const toast = useToast()
function showSuccess() {
toast.add({
severity: 'success',
summary: 'Success',
detail: 'Photo uploaded successfully'
})
}
</script>- Utility-first CSS approach
- Custom PrimeUI integration via
tailwindcss-primeui - Responsive design utilities
- Dark mode support via the
dark:prefix
- Custom Aura theme preset in
style/preset - Consistent color palette
- Component-specific styling overrides
resources/sass/
├── app.css # Main stylesheet entry
└── fonts.css # Fonts styles
Multi-language support through:
- Laravel Vue i18n integration
- Translation files in
lang/directory - Dynamic language switching
- Pluralization and parameter substitution
// In components
$t('gallery.album.create')
// In setup script
import { trans } from "laravel-vue-i18n";
trans('gallery.album.create')- Base URL configuration
- Request/response interceptors
- Caching layer with
axios-cache-interceptor - Error handling and retry logic
- User Action → Component method
- Component → Composable function
- Composable → Service call
- Service → API request
- Response → Store update
- Store → Component reactivity
- Leaflet.js for interactive maps
- Leaflet.markercluster for photo clustering
- Leaflet GPX for GPS track display
- TinyGesture for touch gesture handling
- Mousetrap for keyboard shortcuts
- Vue Collapsed for collapsible content
- ScrollSpy for navigation highlighting
- QR Code generation for sharing
- sprintf-js for string formatting
- VueUse for composition utilities
Lychee deliberately avoids async/await in favor of .then() chains:
// ✅ Preferred in Lychee
AlbumService.get(albumId.value)
.then((response) => {
album.value = response.data
})
.catch((error) => {
console.error(error)
})
// ❌ Avoided in Lychee
// const response = await AlbumService.get(albumId.value)Lychee prefers traditional function declarations:
// ✅ Preferred
function loadPhotos() {
// Implementation
}
// ❌ Avoided
const loadPhotos = () => {
// Implementation
}Common patterns for managing reactive state:
// Single items with computed fallbacks
const selectedPhoto = ref<Photo | undefined>()
const hasPhoto = computed(() => selectedPhoto.value !== undefined)
// Arrays with computed filters
const photos = ref<Photo[]>([])
const favoritePhotos = computed(() =>
photos.value.filter(photo => photo.is_highlighted)
)
// Complex state with multiple refs
const isLoading = ref(false)
const error = ref<string | null>(null)
const data = ref<ApiResponse | undefined>()- Components use
import()for code splitting - Images loaded progressively with intersection observer
- Virtual scrolling for large photo sets
- Thumbnail caching at multiple resolutions
- API response caching with axios-cache-interceptor
- State persistence with Pinia
- Component cleanup in
onUnmountedhooks - Event listener removal
- Touch gesture support with TinyGesture
- Responsive image sizing
- Mobile-first component design
- Performance-conscious animations
- Prefer
computed()over watchers when possible - Implement proper cleanup in
onUnmounted()
export function usePhotoSelection() {
const selectedPhotos = ref<Photo[]>([])
function selectPhoto(photo: Photo) {
selectedPhotos.value.push(photo)
}
function clearSelection() {
selectedPhotos.value = []
}
return {
selectedPhotos,
selectPhoto,
clearSelection
}
}# Start development server with hot reload
npm run dev
# TypeScript type checking
npm run check
# Linting and formatting
npm run lint
npm run format# Production build
npm run build- ESLint configuration for Vue 3 + TypeScript
- Prettier for consistent code formatting
- TypeScript strict mode for type safety
- Vue Component Analyzer for composition API analysis
- Full TypeScript coverage
- Strict type checking enabled
- Custom type definitions for API responses
- Vue DevTools integration
- Hot module replacement (HMR)
- Source map support for debugging
- Consistent naming conventions
- Component composition patterns
- Error boundary implementation
- Accessibility considerations
- Always use
<script setup lang="ts">with TypeScript - Prefer
.then()overasync/await - Use traditional function declarations
- Leverage composables for reusable logic
- Implement proper TypeScript typing
- Use Pinia for complex state management
- Follow PrimeVue component patterns
- Implement proper cleanup and memory management
For more detailed information about specific aspects of the frontend:
- Frontend Gallery Views - Gallery interface, viewing modes, and component architecture
- Frontend Layout System - Photo layout algorithms and responsive design patterns
- Coding Conventions - Coding standards including Vue3/TypeScript conventions
Last updated: December 22, 2025