Skip to main content

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:
tsconfig.json
{
  // 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:
nuxt.config.ts
// 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:
composables/useSound.ts
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.3s 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:
stores/tournament.ts
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:
yarn codegen
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>

Form Handling

<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

Build docs developers (and LLMs) love