Overview
5Stack follows consistent coding conventions to maintain readability and code quality. This guide covers TypeScript, Vue, and general coding practices used throughout the project.
Technology Stack
The project is built with:
Nuxt 3 - Vue.js framework with SSR disabled (SPA mode)
TypeScript - Type-safe JavaScript
Vue 3 - Composition API with script setup
Tailwind CSS - Utility-first CSS framework
shadcn-vue - UI component library
Pinia - State management
GraphQL - API communication with Apollo Client
TypeScript Configuration
The project uses TypeScript with strict mode enabled:
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends" : "./.nuxt/tsconfig.json"
}
TypeScript configuration extends from Nuxt’s auto-generated config, which includes strict type checking and proper Vue type support.
Vue Components
Component Structure
Use the Composition API with <script setup> syntax:
< script setup lang = "ts" >
import { ref , computed } from 'vue'
// Props definition
interface Props {
title : string
count ?: number
}
const props = defineProps < Props >()
// Composables
const { t } = useI18n ()
// State
const isLoading = ref ( false )
// Computed properties
const displayText = computed (() => {
return ` ${ props . title } : ${ props . count ?? 0 } `
})
// Methods
const handleClick = () => {
isLoading . value = true
// Handle action
}
</ script >
< template >
< div >
< h2 > {{ displayText }} </ h2 >
< button @ click = " handleClick " : disabled = " isLoading " >
{{ t ( 'common.submit' ) }}
</ button >
</ div >
</ template >
Component Naming
PascalCase for component files: UserProfile.vue, MatchCard.vue
kebab-case in templates: <user-profile />, <match-card />
Auto-imports Disabled for Components
The project has component auto-imports disabled:
// disable auto imports for components
components : {
dirs : [],
}
This means you must explicitly import components:
< script setup lang = "ts" >
import Button from '~/components/ui/button/Button.vue'
import Card from '~/components/ui/card/Card.vue'
</ script >
Composables
Creating Composables
Composables provide reusable logic. Place them in the composables/ directory:
import { ref } from "vue"
export const useSound = () => {
const isEnabled = ref ( true )
const volume = ref ( 0.7 )
const loadSettings = () => {
if ( ! import . meta . client ) {
return
}
const savedEnabled = localStorage . getItem ( "chat-sound-enabled" )
const savedVolume = localStorage . getItem ( "chat-sound-volume" )
if ( savedEnabled !== null ) {
isEnabled . value = savedEnabled === "true"
}
if ( savedVolume !== null ) {
volume . value = parseFloat ( savedVolume )
}
}
const saveSettings = () => {
if ( ! import . meta . client ) {
return
}
localStorage . setItem ( "chat-sound-enabled" , isEnabled . value . toString ())
localStorage . setItem ( "chat-sound-volume" , volume . value . toString ())
}
if ( import . meta . client ) {
loadSettings ()
}
return {
volume: readonly ( volume ),
isEnabled: readonly ( isEnabled ),
saveSettings ,
}
}
Usage
Composables are auto-imported:
< script setup lang = "ts" >
const { isEnabled , volume , saveSettings } = useSound ()
</ script >
TypeScript Patterns
Type Definitions
Define interfaces for complex types:
interface Tournament {
id : string
name : string
startDate : string
status : 'upcoming' | 'live' | 'finished'
maxTeams : number
}
interface TournamentFilters {
status ?: Tournament [ 'status' ][]
search ?: string
organizerId ?: string
}
Function Types
Use proper typing for functions:
const formatDate = ( date : string , format : 'short' | 'long' = 'short' ) : string => {
// Implementation
return formattedDate
}
const handleSubmit = async ( data : FormData ) : Promise < void > => {
// Implementation
}
Nullability
Handle optional values properly:
// Use optional chaining
const teamName = tournament ?. team ?. name
// Use nullish coalescing
const count = props . count ?? 0
// Type guards
if ( user && user . role === 'admin' ) {
// user is guaranteed to exist here
}
Styling
Tailwind CSS
Use Tailwind utility classes for styling:
< template >
< div class = "flex items-center gap-4 rounded-lg bg-card p-4" >
< h2 class = "text-xl font-semibold text-foreground" >
{{ title }}
</ h2 >
< button class = "rounded-md bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90" >
Submit
</ button >
</ div >
</ template >
Custom CSS
For custom styles, use Tailwind’s @apply directive or scoped styles:
< style scoped >
.custom-component {
@ apply flex flex-col gap- 2;
}
/* Or regular CSS */
.special-animation {
animation : fadeIn 0.3 s ease-in-out ;
}
@keyframes fadeIn {
from { opacity : 0 ; }
to { opacity : 1 ; }
}
</ style >
Prettier Configuration
The project uses Prettier with specific ignore rules:
generated
.nuxt
components/ui
i18n
Key ignored directories:
generated/ - Auto-generated GraphQL types
.nuxt/ - Nuxt build output
components/ui/ - shadcn-vue components
i18n/ - Translation files (maintained manually)
State Management
Pinia Stores
Use Pinia for global state management:
import { defineStore } from 'pinia'
import { ref , computed } from 'vue'
export const useTournamentStore = defineStore ( 'tournament' , () => {
// State
const tournaments = ref < Tournament []>([])
const loading = ref ( false )
// Getters
const liveTournaments = computed (() => {
return tournaments . value . filter ( t => t . status === 'live' )
})
// Actions
const fetchTournaments = async () => {
loading . value = true
try {
// Fetch data
tournaments . value = await api . getTournaments ()
} finally {
loading . value = false
}
}
return {
tournaments ,
loading ,
liveTournaments ,
fetchTournaments ,
}
})
GraphQL
Code Generation
The project uses graphql-zeus for type-safe GraphQL:
This generates TypeScript types from your GraphQL schema.
Using GraphQL
import { useQuery } from '@vue/apollo-composable'
import gql from 'graphql-tag'
const GET_TOURNAMENTS = gql `
query GetTournaments($status: String) {
tournaments(where: { status: { _eq: $status } }) {
id
name
startDate
status
}
}
`
const { result , loading , error } = useQuery ( GET_TOURNAMENTS , {
status: 'live'
})
Client-Side Only Code
The project runs in SPA mode (ssr: false), but always guard browser-only APIs:
// Check for client-side
if ( import . meta . client ) {
// Safe to use localStorage, window, etc.
localStorage . setItem ( 'key' , 'value' )
}
// Or in composables
const useLocalStorage = () => {
if ( ! import . meta . client ) {
return
}
// Browser API usage
}
File Organization
Pages
Pages follow Nuxt’s file-based routing:
pages/
├── index.vue # /
├── tournaments/
│ ├── index.vue # /tournaments
│ ├── create.vue # /tournaments/create
│ └── [tournamentId]/
│ ├── index.vue # /tournaments/:tournamentId
│ └── organizers.vue # /tournaments/:tournamentId/organizers
└── settings/
├── index.vue # /settings
└── api-keys.vue # /settings/api-keys
Components
components/
├── ui/ # UI library components
│ ├── button/
│ ├── card/
│ └── table/
└── [feature]/ # Feature-specific components
├── TournamentCard.vue
└── MatchList.vue
Best Practices
Use Composition API Always use <script setup> with Composition API for consistency
Type Everything Leverage TypeScript’s type system for better DX and fewer bugs
Internationalize Text Never hardcode user-facing text - use i18n keys
Guard Browser APIs Check import.meta.client before using browser-only APIs
Common Patterns
Loading States
< script setup lang = "ts" >
const loading = ref ( false )
const data = ref < Tournament []>([])
const fetchData = async () => {
loading . value = true
try {
data . value = await api . getTournaments ()
} catch ( error ) {
console . error ( 'Failed to fetch tournaments:' , error )
} finally {
loading . value = false
}
}
onMounted (() => {
fetchData ()
})
</ script >
< template >
< div >
< div v-if = " loading " > Loading... </ div >
< div v-else-if = " data . length === 0 " > No data found </ div >
< div v-else >
< TournamentCard v-for = " item in data " : key = " item . id " : tournament = " item " />
</ div >
</ div >
</ template >
< script setup lang = "ts" >
import { useForm } from 'vee-validate'
import { z } from 'zod'
import { toTypedSchema } from '@vee-validate/zod'
const schema = toTypedSchema ( z . object ({
name: z . string (). min ( 3 , 'Name must be at least 3 characters' ),
maxTeams: z . number (). min ( 2 ). max ( 64 )
}))
const { handleSubmit , errors } = useForm ({
validationSchema: schema
})
const onSubmit = handleSubmit ( async ( values ) => {
// Handle form submission
await createTournament ( values )
})
</ script >
Contributing Guide Learn how to contribute to the project
Internationalization Add and manage translations