MediaStream’s frontend is built with Vue.js 3, TypeScript, and a comprehensive set of reusable UI components.
Project Structure
Entry Points
// resources/js/app.ts - Client-side entry
import '../css/app.css' ;
import { createInertiaApp } from '@inertiajs/vue3' ;
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers' ;
import { createApp , h } from 'vue' ;
import { initializeTheme } from './composables/useAppearance' ;
const appName = import . meta . env . VITE_APP_NAME || 'Laravel' ;
createInertiaApp ({
title : ( title ) => ( title ? ` ${ title } - ${ appName } ` : appName ),
resolve : ( name ) =>
resolvePageComponent (
`./pages/ ${ name } .vue` ,
import . meta . glob < DefineComponent >( './pages/**/*.vue' ),
),
setup ({ el , App , props , plugin }) {
createApp ({ render : () => h ( App , props ) })
. use ( plugin )
. mount ( el );
},
progress: {
color: '#4B5563' ,
},
});
initializeTheme ();
TypeScript Configuration
The project uses strict TypeScript with path aliases:
{
"compilerOptions" : {
"target" : "ESNext" ,
"module" : "ESNext" ,
"moduleResolution" : "bundler" ,
"strict" : true ,
"jsx" : "preserve" ,
"jsxImportSource" : "vue" ,
"paths" : {
"@/*" : [ "./resources/js/*" ]
},
"types" : [ "vite/client" , "./resources/js/types" ]
}
}
Use the @/ alias to import from resources/js/. For example: import { useAppearance } from '@/composables/useAppearance'
Type Definitions
Shared Types
Global types are defined in resources/js/types/index.d.ts:
export interface Auth {
user : User ;
}
export interface User {
id : number ;
name : string ;
email : string ;
avatar ?: string ;
email_verified_at : string | null ;
created_at : string ;
updated_at : string ;
}
export type AppPageProps <
T extends Record < string , unknown > = Record < string , unknown >,
> = T & {
name : string ;
quote : { message : string ; author : string };
auth : Auth ;
sidebarOpen : boolean ;
};
export interface NavItem {
title : string ;
href : NonNullable < InertiaLinkProps [ 'href' ]>;
icon ?: LucideIcon ;
isActive ?: boolean ;
children ?: NavItem [];
}
Using Types in Components
< script setup lang = "ts" >
import type { AppPageProps } from '@/types' ;
defineProps <{
data : AppPageProps <{
series : Array <{ id : string ; title : string }>;
}>;
}>();
</ script >
Page Components
Inertia pages live in resources/js/pages/ and use file-based routing:
pages/
├── Dashboard.vue # /dashboard
├── Welcome.vue # /
├── (media)/ # Route group
│ ├── series/
│ │ ├── index.vue # /series
│ │ ├── create/
│ │ │ └── index.vue # /series/create
│ │ └── [showId]/
│ │ └── index.vue # /series/{showId}
│ ├── seasons/
│ │ └── index.vue
│ └── vod/
│ ├── index.vue
│ └── upload/
│ └── index.vue
├── auth/
│ ├── Login.vue
│ ├── Register.vue
│ └── ForgotPassword.vue
└── settings/
├── Profile.vue
└── Security.vue
Folders wrapped in parentheses like (media) are route groups that don’t affect the URL structure.
Example Page Component
< script setup lang = "ts" >
import AppLayout from '@/layouts/AppLayout.vue' ;
import { Head } from '@inertiajs/vue3' ;
defineProps <{
data : Array <{ _id : string ; title : string ; type : string }>;
}>();
</ script >
< template >
< AppLayout >
< Head title = "Series" / >
< div class = "container mx-auto p-6" >
< h1 class = "text-2xl font-bold mb-4" > TV Series </ h1 >
< div v-for = " series in data " : key = " series . _id " >
{{ series . title }}
</ div >
</ div >
</ AppLayout >
</ template >
Layouts
MediaStream uses three main layout components:
AppLayout (Authenticated)
<!-- resources/js/layouts/AppLayout.vue -->
< script setup lang = "ts" >
import AppHeaderLayout from './app/AppHeaderLayout.vue' ;
import AppSidebarLayout from './app/AppSidebarLayout.vue' ;
</ script >
< template >
< div class = "flex h-screen" >
< AppSidebarLayout />
< div class = "flex-1 flex flex-col" >
< AppHeaderLayout />
< main class = "flex-1 overflow-auto" >
< slot / >
</ main >
</ div >
</ div >
</ template >
AuthLayout (Guest)
Used for login, registration, and password reset pages.
SettingsLayout
Used for user settings pages with a dedicated sidebar.
Composables
Vue composables provide reusable logic across components:
useAppearance
Manages light/dark theme with SSR support:
// resources/js/composables/useAppearance.ts
import { onMounted , ref } from 'vue' ;
type Appearance = 'light' | 'dark' | 'system' ;
export function useAppearance () {
const appearance = ref < Appearance >( 'system' );
onMounted (() => {
const savedAppearance = localStorage . getItem ( 'appearance' ) as Appearance | null ;
if ( savedAppearance ) {
appearance . value = savedAppearance ;
}
});
function updateAppearance ( value : Appearance ) {
appearance . value = value ;
localStorage . setItem ( 'appearance' , value );
setCookie ( 'appearance' , value ); // For SSR
updateTheme ( value );
}
return { appearance , updateAppearance };
}
export function initializeTheme () {
// Called on page load to apply saved theme
const savedAppearance = getStoredAppearance ();
updateTheme ( savedAppearance || 'system' );
}
Usage:
< script setup lang = "ts" >
import { useAppearance } from '@/composables/useAppearance' ;
const { appearance , updateAppearance } = useAppearance ();
</ script >
< template >
< select v-model = " appearance " @ change = " updateAppearance ( appearance ) " >
< option value = "light" > Light </ option >
< option value = "dark" > Dark </ option >
< option value = "system" > System </ option >
</ select >
</ template >
useTwoFactorAuth
Handles two-factor authentication setup:
// resources/js/composables/useTwoFactorAuth.ts
import { ref } from 'vue' ;
import { router } from '@inertiajs/vue3' ;
export function useTwoFactorAuth () {
const qrCode = ref < string | null >( null );
const recoveryCodes = ref < string []>([]);
function enable () {
router . post ( '/user/two-factor-authentication' );
}
function disable () {
router . delete ( '/user/two-factor-authentication' );
}
return { qrCode , recoveryCodes , enable , disable };
}
useInitials
Generates user initials for avatars:
// resources/js/composables/useInitials.ts
export function useInitials ( name : string ) : string {
return name
. split ( ' ' )
. map ( word => word [ 0 ])
. join ( '' )
. toUpperCase ()
. slice ( 0 , 2 );
}
UI Components
MediaStream includes 23+ reusable UI components built with reka-ui (headless primitives):
resources/js/components/ui/
├── alert/
├── avatar/
├── badge/
├── breadcrumb/
├── button/
├── card/
├── checkbox/
├── collapsible/
├── dialog/
├── dropdown-menu/
├── form/
├── input/
├── label/
├── select/
├── separator/
├── sheet/
├── skeleton/
├── switch/
├── table/
├── tabs/
├── textarea/
├── toast/
└── tooltip/
< script setup lang = "ts" >
import { Button } from '@/components/ui/button' ;
import { Loader2 } from 'lucide-vue-next' ;
</ script >
< template >
< Button variant = "primary" size = "lg" : disabled = " loading " >
< Loader2 v-if = " loading " class = "mr-2 h-4 w-4 animate-spin" />
Save Changes
</ Button >
</ template >
Forms use vee-validate with Zod schemas:
< script setup lang = "ts" >
import { useForm } from 'vee-validate' ;
import { toTypedSchema } from '@vee-validate/zod' ;
import * as z from 'zod' ;
import { router } from '@inertiajs/vue3' ;
const schema = toTypedSchema (
z . object ({
series_name: z . string (). min ( 1 , 'Title is required' ),
series_type: z . enum ([ 'tvshow' , 'movie' ]),
})
);
const { handleSubmit , errors } = useForm ({
validationSchema: schema ,
});
const onSubmit = handleSubmit (( values ) => {
router . post ( '/series' , values );
});
</ script >
< template >
< form @ submit = " onSubmit " >
< FormField name = "series_name" v-slot = " { field } " >
< FormItem >
< FormLabel > Series Title </ FormLabel >
< FormControl >
< Input v-bind = " field " />
</ FormControl >
< FormMessage />
</ FormItem >
</ FormField >
< Button type = "submit" > Create Series </ Button >
</ form >
</ template >
Icons
MediaStream uses lucide-vue-next for icons:
< script setup lang = "ts" >
import { Film , Tv , Upload , Settings } from 'lucide-vue-next' ;
</ script >
< template >
< div class = "flex gap-2" >
< Film class = "h-5 w-5" />
< Tv class = "h-5 w-5" />
< Upload class = "h-5 w-5" />
< Settings class = "h-5 w-5" />
</ div >
</ template >
Browse all icons at lucide.dev .
Styling with Tailwind CSS
MediaStream uses Tailwind CSS 4 with the Vite plugin:
// vite.config.ts
import tailwindcss from '@tailwindcss/vite' ;
export default defineConfig ({
plugins: [
tailwindcss (),
] ,
}) ;
Utility Functions
For conditional classes, use clsx and tailwind-merge:
import { clsx , type ClassValue } from 'clsx' ;
import { twMerge } from 'tailwind-merge' ;
export function cn ( ... inputs : ClassValue []) {
return twMerge ( clsx ( inputs ));
}
Usage:
< div : class = " cn ( 'px-4 py-2' , isActive && 'bg-blue-500' , className ) " />
Inertia.js Patterns
Making Requests
import { router } from '@inertiajs/vue3' ;
// GET request (navigation)
router . get ( '/series' );
// POST request
router . post ( '/series' , { title: 'New Series' , type: 'tvshow' });
// PUT request
router . put ( `/series/ ${ id } ` , formData );
// DELETE request
router . delete ( `/series/ ${ id } ` , {
onSuccess : () => {
console . log ( 'Series deleted' );
},
});
Links
< script setup lang = "ts" >
import { Link } from '@inertiajs/vue3' ;
</ script >
< template >
< Link href = "/series" class = "text-blue-600 hover:underline" >
View All Series
</ Link >
</ template >
Accessing Page Props
< script setup lang = "ts" >
import { usePage } from '@inertiajs/vue3' ;
import type { AppPageProps } from '@/types' ;
const page = usePage < AppPageProps >();
const user = computed (() => page . props . auth . user );
</ script >
< template >
< div > Welcome, {{ user . name }}! </ div >
</ template >
Development Workflow
Running the Dev Server
# Start all services (Laravel + Vite + Queue + Logs)
composer dev
# Or run separately:
npm run dev # Vite dev server
php artisan serve # Laravel server
Type Checking
# Check TypeScript types
npm run type-check # If configured
vue-tsc --noEmit # Manual check
Linting
npm run lint # ESLint with auto-fix
npm run format # Prettier
Best Practices
Keep page components simple, extract complex logic to composables
Use TypeScript interfaces for all props and emits
Prefer composition API over options API
Extract reusable UI patterns to components/custom/
Use Inertia’s shared data for global state (user, settings)
Use composables for shared logic (not Pinia/Vuex)
Keep component state local when possible
Use props and events for parent-child communication
Enable strict mode (already configured)
Define interfaces for all API responses
Use AppPageProps<T> for page component props
Avoid any - use unknown and type guards instead
Next Steps
Backend Development Learn about Laravel controllers and services
Database Schema Explore the database structure