๐Ÿ””Welcome

HaloLight multi-framework admin dashboard docs is now live!

Supports 12+ framework versions. Welcome to try.

Skip to content

Qwik Version โ€‹

HaloLight Qwik version is built on Qwik City, featuring Qwik resumability architecture + TypeScript for zero hydration and ultimate performance.

Live Preview: https://halolight-qwik.h7ml.cn/

GitHub: https://github.com/halolight/halolight-qwik

Tech Stack โ€‹

TechnologyVersionDescription
Qwik2.xResumable framework
Qwik City2.xFull-stack framework
TypeScript5.xType safety
Tailwind CSS4.xAtomic CSS
Qwik UIlatestUI component library
Modular FormslatestForm handling
Zod3.xData validation
ECharts5.xChart visualization
Mock.js1.xData mocking

Core Features โ€‹

  • Resumability: No hydration needed, server state directly resumed
  • Lazy Load Everything: Code loaded on demand, minimal initial JS
  • Signals: Fine-grained reactivity
  • Server-side Rendering: Built-in SSR support
  • File-based Routing: Directory-based routing system
  • Edge Deployment: Native support for Cloudflare Workers and other edge platforms

Directory Structure โ€‹

halolight-qwik/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ routes/                    # File-based routing
โ”‚   โ”‚   โ”œโ”€โ”€ index.tsx            # Home
โ”‚   โ”‚   โ”œโ”€โ”€ layout.tsx           # Root layout
โ”‚   โ”‚   โ”œโ”€โ”€ (auth)/              # Auth route group
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ layout.tsx
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ login/
โ”‚   โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ index.tsx
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ register/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ forgot-password/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ reset-password/
โ”‚   โ”‚   โ”œโ”€โ”€ (dashboard)/         # Dashboard route group
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ layout.tsx
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ dashboard/
โ”‚   โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ index.tsx
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ users/
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ index.tsx
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ create/
โ”‚   โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ [id]/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ roles/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ permissions/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ settings/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ profile/
โ”‚   โ”‚   โ””โ”€โ”€ api/                 # API endpoints
โ”‚   โ”‚       โ””โ”€โ”€ auth/
โ”‚   โ”‚           โ””โ”€โ”€ login/
โ”‚   โ”‚               โ””โ”€โ”€ index.ts
โ”‚   โ”œโ”€โ”€ components/              # Component library
โ”‚   โ”‚   โ”œโ”€โ”€ ui/                  # Qwik UI components
โ”‚   โ”‚   โ”œโ”€โ”€ layout/              # Layout components
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ admin-layout/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ auth-layout/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ sidebar/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ header/
โ”‚   โ”‚   โ”œโ”€โ”€ dashboard/           # Dashboard components
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ dashboard-grid/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ widget-wrapper/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ stats-widget/
โ”‚   โ”‚   โ””โ”€โ”€ shared/              # Shared components
โ”‚   โ”‚       โ””โ”€โ”€ permission-guard/
โ”‚   โ”œโ”€โ”€ stores/                  # State management
โ”‚   โ”‚   โ”œโ”€โ”€ auth.ts
โ”‚   โ”‚   โ”œโ”€โ”€ ui-settings.ts
โ”‚   โ”‚   โ””โ”€โ”€ dashboard.ts
โ”‚   โ”œโ”€โ”€ lib/                     # Utilities
โ”‚   โ”‚   โ”œโ”€โ”€ api.ts
โ”‚   โ”‚   โ”œโ”€โ”€ permission.ts
โ”‚   โ”‚   โ””โ”€โ”€ cn.ts
โ”‚   โ””โ”€โ”€ types/                   # Type definitions
โ”œโ”€โ”€ public/                      # Static assets
โ”œโ”€โ”€ vite.config.ts              # Vite config
โ”œโ”€โ”€ tailwind.config.ts          # Tailwind config
โ””โ”€โ”€ package.json

Quick Start โ€‹

Installation โ€‹

bash
git clone https://github.com/halolight/halolight-qwik.git
cd halolight-qwik
pnpm install

Environment Variables โ€‹

bash
cp .env.example .env
env
# .env example
VITE_API_URL=/api
VITE_USE_MOCK=true
VITE_DEMO_EMAIL=admin@halolight.h7ml.cn
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=true
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

Start Development โ€‹

bash
pnpm dev

Visit http://localhost:5173

Build for Production โ€‹

bash
pnpm build
pnpm serve

Core Features โ€‹

State Management (Context + Signals) โ€‹

tsx
// stores/auth.ts
import {
  createContextId,
  useContext,
  useStore,
  useComputed$,
  $,
  type Signal,
} from '@builder.io/qwik'

interface User {
  id: number
  name: string
  email: string
  permissions: string[]
}

interface AuthState {
  user: User | null
  token: string | null
  loading: boolean
}

export const AuthContext = createContextId<AuthState>('auth')

export function useAuth() {
  const state = useContext(AuthContext)

  const isAuthenticated = useComputed$(() => !!state.token && !!state.user)
  const permissions = useComputed$(() => state.user?.permissions ?? [])

  const login = $(async (credentials: { email: string; password: string }) => {
    state.loading = true
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        body: JSON.stringify(credentials),
        headers: { 'Content-Type': 'application/json' },
      })
      const data = await response.json()

      state.user = data.user
      state.token = data.token
    } finally {
      state.loading = false
    }
  })

  const logout = $(() => {
    state.user = null
    state.token = null
  })

  const hasPermission = $((permission: string) => {
    const perms = state.user?.permissions ?? []
    return perms.some(p =>
      p === '*' || p === permission ||
      (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
    )
  })

  return {
    state,
    isAuthenticated,
    permissions,
    login,
    logout,
    hasPermission,
  }
}

Root Layout (Provide Context) โ€‹

tsx
// routes/layout.tsx
import { component$, Slot, useContextProvider, useStore } from '@builder.io/qwik'
import { AuthContext } from '~/stores/auth'

export default component$(() => {
  const authState = useStore({
    user: null,
    token: null,
    loading: false,
  })

  useContextProvider(AuthContext, authState)

  return <Slot />
})

Route Guards โ€‹

tsx
// routes/(dashboard)/layout.tsx
import { component$, Slot } from '@builder.io/qwik'
import { routeLoader$, useNavigate } from '@builder.io/qwik-city'
import { useAuth } from '~/stores/auth'
import { AdminLayout } from '~/components/layout/admin-layout'

export const useAuthGuard = routeLoader$(async ({ cookie, redirect, url }) => {
  const token = cookie.get('token')?.value

  if (!token) {
    throw redirect(302, `/login?redirect=${url.pathname}`)
  }

  // Validate token and return user info
  return {
    user: await validateToken(token),
  }
})

export default component$(() => {
  const data = useAuthGuard()

  return (
    <AdminLayout user={data.value.user}>
      <Slot />
    </AdminLayout>
  )
})

Data Loading (routeLoader$) โ€‹

tsx
// routes/(dashboard)/users/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeLoader$ } from '@builder.io/qwik-city'

export const useUsers = routeLoader$(async ({ query, cookie, status }) => {
  const token = cookie.get('token')?.value
  const page = Number(query.get('page')) || 1

  // Permission check
  const user = await validateToken(token)
  if (!hasPermission(user, 'users:list')) {
    status(403)
    return { error: 'No permission to access' }
  }

  const response = await fetch(`/api/users?page=${page}`, {
    headers: { Authorization: `Bearer ${token}` },
  })

  return response.json()
})

export default component$(() => {
  const users = useUsers()

  return (
    <div>
      <h1>User List</h1>

      {users.value.error ? (
        <div class="text-destructive">{users.value.error}</div>
      ) : (
        <ul>
          {users.value.data.map((user) => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      )}
    </div>
  )
})

Server Actions (routeAction$) โ€‹

tsx
// routes/(auth)/login/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeAction$, zod$, z, Form } from '@builder.io/qwik-city'

export const useLogin = routeAction$(
  async (data, { cookie, redirect, fail }) => {
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        body: JSON.stringify(data),
        headers: { 'Content-Type': 'application/json' },
      })

      if (!response.ok) {
        return fail(401, { message: 'Invalid email or password' })
      }

      const result = await response.json()

      cookie.set('token', result.token, {
        path: '/',
        httpOnly: true,
        sameSite: 'strict',
        maxAge: 60 * 60 * 24 * 7,
      })

      throw redirect(302, '/dashboard')
    } catch (e) {
      return fail(500, { message: 'Server error' })
    }
  },
  zod$({
    email: z.string().email('Please enter a valid email'),
    password: z.string().min(6, 'Password must be at least 6 characters'),
  })
)

export default component$(() => {
  const action = useLogin()

  return (
    <Form action={action}>
      {action.value?.failed && (
        <div class="text-destructive">{action.value.message}</div>
      )}

      <input type="email" name="email" placeholder="Email" />
      {action.value?.fieldErrors?.email && (
        <span class="text-destructive">{action.value.fieldErrors.email}</span>
      )}

      <input type="password" name="password" placeholder="Password" />
      {action.value?.fieldErrors?.password && (
        <span class="text-destructive">{action.value.fieldErrors.password}</span>
      )}

      <button type="submit" disabled={action.isRunning}>
        {action.isRunning ? 'Logging in...' : 'Login'}
      </button>
    </Form>
  )
})

Permission Component โ€‹

tsx
// components/shared/permission-guard/index.tsx
import { component$, Slot, useComputed$ } from '@builder.io/qwik'
import { useAuth } from '~/stores/auth'

interface Props {
  permission: string
}

export const PermissionGuard = component$<Props>(({ permission }) => {
  const { hasPermission } = useAuth()

  const allowed = useComputed$(async () => {
    return await hasPermission(permission)
  })

  return (
    <>
      {allowed.value ? (
        <Slot />
      ) : (
        <Slot name="fallback" />
      )}
    </>
  )
})
tsx
// Usage
<PermissionGuard permission="users:delete">
  <Button variant="destructive" q:slot="">Delete</Button>
  <span q:slot="fallback" class="text-muted-foreground">No Permission</span>
</PermissionGuard>

API Endpoints โ€‹

ts
// routes/api/auth/login/index.ts
import type { RequestHandler } from '@builder.io/qwik-city'

export const onPost: RequestHandler = async ({ json, parseBody }) => {
  const body = await parseBody()
  const { email, password } = body as { email: string; password: string }

  // Validation logic
  if (!email || !password) {
    json(400, { success: false, message: 'Email and password are required' })
    return
  }

  // Authentication logic...

  json(200, {
    success: true,
    user: { id: 1, name: 'Admin', email },
    token: 'mock_token',
  })
}

Page Routes โ€‹

PathPagePermission
/HomePublic
/loginLoginPublic
/registerRegisterPublic
/forgot-passwordForgot PasswordPublic
/reset-passwordReset PasswordPublic
/dashboardDashboarddashboard:view
/usersUser Listusers:list
/users/createCreate Userusers:create
/users/[id]User Detailsusers:view
/rolesRole Managementroles:list
/permissionsPermission Managementpermissions:list
/settingsSystem Settingssettings:view
/profileProfileAuthenticated

Configuration โ€‹

Vite Configuration โ€‹

ts
// vite.config.ts
import { defineConfig } from 'vite'
import { qwikVite } from '@builder.io/qwik/optimizer'
import { qwikCity } from '@builder.io/qwik-city/vite'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig(() => {
  return {
    plugins: [qwikCity(), qwikVite(), tsconfigPaths()],
  }
})

Deployment โ€‹

Node.js Server โ€‹

bash
pnpm build
node server/entry.express.js

Docker โ€‹

dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/server ./server
COPY --from=builder /app/package.json .
RUN npm install --production
EXPOSE 3000
CMD ["node", "server/entry.express.js"]

Cloudflare Pages โ€‹

bash
# Use Cloudflare Pages adapter
pnpm add -D @builder.io/qwik-city/adapters/cloudflare-pages

Vercel โ€‹

bash
# Use Vercel Edge adapter
pnpm add -D @builder.io/qwik-city/adapters/vercel-edge

Testing โ€‹

bash
# Run tests
pnpm test

# E2E tests
pnpm test.e2e

Comparison with Other Versions โ€‹

FeatureQwik VersionVue VersionNext.js Version
State ManagementContext + SignalsPiniaZustand
Data FetchingrouteLoader$TanStack QueryTanStack Query
Form ValidationModular Forms + ZodVeeValidate + ZodReact Hook Form + Zod
Server-sideBuilt-inSeparate BackendAPI Routes
Component LibraryQwik UIshadcn-vueshadcn/ui
RoutingFile-based RoutingVue RouterApp Router
HydrationResumable (Zero Hydration)Traditional HydrationTraditional Hydration
Initial JS~1KB~33KB~85KB