This commit is contained in:
Haim Kortovich
2026-03-05 11:36:23 -05:00
commit 7e8025700b
19 changed files with 13483 additions and 0 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

75
README.md Normal file
View File

@@ -0,0 +1,75 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

8
app/app.vue Normal file
View File

@@ -0,0 +1,8 @@
<template>
<UApp>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</UApp>
</template>

2
app/assets/css/main.css Normal file
View File

@@ -0,0 +1,2 @@
@import "tailwindcss";
@import "@nuxt/ui";

View File

@@ -0,0 +1,75 @@
<script setup lang="ts">
import type { CustomerResponse } from '#open-fetch'
defineProps<{
customer: CustomerResponse<any>
}>()
</script>
<template>
<UCard
class="bg-gradient-to-b from-white to-slate-50
border border-slate-200 shadow-sm rounded-xl"
>
<template #header>
<div class="flex items-center justify-between">
<!-- Avatar + Name -->
<div class="flex items-center gap-3">
<UAvatar
:alt="customer.first_name"
size="lg"
class="bg-primary-100 text-primary-700"
/>
<div>
<div class="font-semibold text-slate-900 text-lg">
{{ customer.first_name }} {{ customer.last_name }}
</div>
<div class="text-sm text-slate-500">
{{ customer.email }}
</div>
</div>
</div>
</div>
</template>
<div class="space-y-3 text-sm mt-4">
<div class="flex justify-between">
<span class="text-slate-500">Birth Date</span>
<span class="text-slate-700">{{ customer.birth_date }}</span>
</div>
<div class="flex justify-between">
<span class="text-slate-500">Gender</span>
<span class="text-slate-700 capitalize">
{{ customer.gender }}
</span>
</div>
<div class="flex justify-between">
<span class="text-slate-500">Phone</span>
<span class="text-slate-700">
{{ customer.phone }}
</span>
</div>
</div>
<template #footer>
<div class="flex justify-between pt-4 border-t border-slate-100">
<UButton
as="NuxtLink"
size="xs"
variant="ghost"
:to="{ name: 'customers-id', params: { id: customer.id } }"
>
View
</UButton>
<UButton size="xs" variant="ghost" color="primary">
Policies
</UButton>
</div>
</template>
</UCard>
</template>

53
app/layouts/default.vue Normal file
View File

@@ -0,0 +1,53 @@
<template>
<div class="min-h-screen bg-slate-100 flex">
<!-- Sidebar -->
<aside
class="w-64 bg-white border-r border-slate-200 hidden md:flex flex-col"
>
<!-- Brand -->
<div class="h-16 flex items-center px-6 border-b border-slate-200">
<span class="text-lg font-semibold tracking-tight">
PolicyManager
</span>
</div>
<!-- Navigation -->
<nav class="flex-1 p-4 space-y-1">
<NuxtLink
to="/"
class="group flex items-center px-3 py-2 rounded-lg text-sm font-medium
text-slate-600 hover:bg-slate-100 hover:text-slate-900 transition"
>
Dashboard
</NuxtLink>
<NuxtLink
to="/customers"
class="group flex items-center px-3 py-2 rounded-lg text-sm font-medium
bg-primary-50 text-primary-700 border border-primary-100"
>
Customers
</NuxtLink>
<NuxtLink
to="/policies"
class="group flex items-center px-3 py-2 rounded-lg text-sm font-medium
text-slate-600 hover:bg-slate-100 hover:text-slate-900 transition"
>
Policies
</NuxtLink>
</nav>
</aside>
<!-- Main -->
<main class="flex-1">
<div class="p-8">
<NuxtPage />
</div>
</main>
</div>
</template>

View File

@@ -0,0 +1,99 @@
<script setup lang="ts">
const route = useRoute()
const { data, error, pending } = useCustomer(route.params.id)
</script>
<template>
<div class="p-8 bg-slate-100 min-h-screen">
<!-- Loading -->
<div v-if="pending" class="max-w-3xl">
<UCard class="bg-white shadow-sm border border-slate-200">
<div class="h-48 animate-pulse bg-slate-200 rounded" />
</UCard>
</div>
<!-- Error -->
<UAlert
v-else-if="error"
color="red"
variant="soft"
title="Failed to load customer"
:description="error.message"
/>
<!-- Customer View -->
<div v-else-if="data" class="max-w-4xl space-y-6">
<!-- Header Card -->
<UCard class="bg-white border border-slate-200 shadow-sm rounded-xl">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<UAvatar size="xl" :alt="data.first_name" />
<div>
<h1 class="text-2xl font-semibold text-slate-900">
{{ data.first_name }} {{ data.last_name }}
</h1>
<p class="text-slate-500 text-sm">
{{ data.email }}
</p>
</div>
</div>
</div>
</UCard>
<!-- Details Card -->
<UCard class="bg-white border border-slate-200 shadow-sm rounded-xl">
<h2 class="text-lg font-semibold text-slate-800 mb-4">
Personal Information
</h2>
<div class="grid grid-cols-2 gap-6 text-sm">
<div>
<div class="text-slate-500">Birth Date</div>
<div class="text-slate-800 font-medium">
{{ data.birth_date }}
</div>
</div>
<div>
<div class="text-slate-500">Gender</div>
<div class="text-slate-800 font-medium capitalize">
{{ data.gender }}
</div>
</div>
<div>
<div class="text-slate-500">Phone</div>
<div class="text-slate-800 font-medium">
{{ data.phone }}
</div>
</div>
<div>
<div class="text-slate-500">Customer ID</div>
<div class="text-slate-800 font-mono text-xs">
{{ data.id }}
</div>
</div>
</div>
</UCard>
<!-- Placeholder for Policies -->
<UCard class="bg-white border border-slate-200 shadow-sm rounded-xl">
<h2 class="text-lg font-semibold text-slate-800 mb-4">
Policies
</h2>
<div class="text-slate-500 text-sm">
No policies yet.
</div>
</UCard>
</div>
</div>
</template>

View File

@@ -0,0 +1,65 @@
<script setup lang="ts">
const { data, error, pending } = useCustomer('/')
const search = ref('')
const filtered = computed(() => {
if (!data.value) return []
if (!search.value) return data.value
return data.value.filter((c: any) =>
`${c.first_name} ${c.last_name} ${c.email}`
.toLowerCase()
.includes(search.value.toLowerCase())
)
})
const total = computed(() => data.value?.length ?? 0)
</script>
<template>
<div class="p-8 space-y-8 bg-gray-50 min-h-screen">
<!-- Header -->
<div class="flex justify-between items-center">
<div>
<h1 class="text-3xl text-slate-900 font-bold">Customers</h1>
<p class="text-gray-500 text-sm">Customer Relationship Management</p>
</div>
<UButton icon="i-heroicons-plus" color="primary" to="/customers/new">
New Customer
</UButton>
</div>
<!-- Error -->
<UAlert
v-if="error"
color="red"
variant="soft"
title="Failed to load customers"
:description="error.message"
/>
<!-- Loading -->
<div
v-else-if="pending"
class="grid gap-6 md:grid-cols-2 lg:grid-cols-3"
>
<UCard v-for="n in 6" :key="n">
<div class="h-32 animate-pulse bg-gray-200 rounded" />
</UCard>
</div>
<!-- Customer Grid -->
<div
v-else
class="grid gap-6 md:grid-cols-2 lg:grid-cols-3"
>
<CustomerCard
v-for="customer in filtered"
:key="customer.id"
:customer="customer"
/>
</div>
</div>
</template>

126
app/pages/customers/new.vue Normal file
View File

@@ -0,0 +1,126 @@
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = z.object({
first_name: z.string().min(1, 'First name is required'),
last_name: z.string().min(1, 'Last name is required'),
email: z.string().email('Invalid email'),
phone: z.string().min(1, 'Phone is required'),
birth_date: z.string().min(1, 'Birth date is required'),
gender: z.enum(['male', 'female'])
})
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({
first_name: '',
last_name: '',
email: '',
phone: '',
birth_date: '',
gender: 'male'
})
const toast = useToast()
const router = useRouter()
async function onSubmit(event: FormSubmitEvent<Schema>) {
try {
await useCustomer('/', {
method: 'POST',
body: event.data
})
toast.add({
title: 'Customer created',
description: 'The customer was successfully created.',
color: 'success'
})
router.push('/customers')
} catch (err: any) {
toast.add({
title: 'Error',
description: err?.message || 'Failed to create customer',
color: 'error'
})
}
}
</script>
<template>
<div class="min-h-screen bg-gray-50 p-10">
<div class="max-w-5xl mx-auto space-y-8">
<!-- Header -->
<div>
<h1 class="text-3xl font-bold text-gray-900">New Customer</h1>
<p class="text-gray-500 mt-1">Create a new customer profile</p>
</div>
<!-- Form Card -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
<UForm
:schema="schema"
:state="state"
class="space-y-6"
@submit="onSubmit"
>
<!-- Grid -->
<div class="grid md:grid-cols-2 gap-6">
<UFormField label="First Name" name="first_name">
<UInput v-model="state.first_name" />
</UFormField>
<UFormField label="Last Name" name="last_name">
<UInput v-model="state.last_name" />
</UFormField>
<UFormField label="Email" name="email">
<UInput v-model="state.email" />
</UFormField>
<UFormField label="Phone" name="phone">
<UInput v-model="state.phone" />
</UFormField>
<UFormField label="Birth Date" name="birth_date">
<UInput v-model="state.birth_date" type="date" />
</UFormField>
<UFormField label="Gender" name="gender">
<USelect
v-model="state.gender"
:items="[
{ label: 'Male', value: 'male' },
{ label: 'Female', value: 'female' }
]"
/>
</UFormField>
</div>
<!-- Actions -->
<div class="flex justify-end gap-4 pt-4">
<UButton
color="neutral"
variant="ghost"
@click="$router.push('/customers')"
>
Cancel
</UButton>
<UButton type="submit" color="primary">
Create Customer
</UButton>
</div>
</UForm>
</div>
</div>
</div>
</template>

11
app/pages/index.vue Normal file
View File

@@ -0,0 +1,11 @@
<template>
<UContainer class="py-10">
<UCard>
<template #header>
<h1 class="text-2xl font-bold">
</h1>
</template>
</UCard>
</UContainer>
</template>

60
flake.lock generated Normal file
View File

@@ -0,0 +1,60 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1772052852,
"narHash": "sha256-dPHNrQKkUzgaS8ztwMvuLkHCcHWQ6yfRSvRfT3/pTeU=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "51b1a06c971d633d6b77c3e894f8deaadddd5cfb",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

24
flake.nix Normal file
View File

@@ -0,0 +1,24 @@
{
description = "test";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
devShell =
pkgs.mkShell {
buildInputs = with pkgs;
[
nodejs
vue-language-server
];
};
});
}

16
nuxt.config.ts Normal file
View File

@@ -0,0 +1,16 @@
export default defineNuxtConfig({
compatibilityDate: '2026-02-25',
modules: ['nuxt-open-fetch', '@nuxt/ui'],
css: ['~/assets/css/main.css'],
ui: {
colorMode: false
},
openFetch: {
clients: {
customer: {
baseURL: 'http://localhost:4000/api/customers',
schema: 'http://localhost:4000/api/openapi'
}
}
}
})

12803
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "policy-ui",
"type": "module",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/ui": "^4.5.0",
"nuxt": "^4.3.1",
"nuxt-open-fetch": "^0.13.8",
"tailwindcss": "^4.2.1",
"vue": "^3.5.28",
"vue-router": "^4.6.4",
"zod": "^4.3.6"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-Agent: *
Disallow:

18
tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"files": [],
"references": [
{
"path": "./.nuxt/tsconfig.app.json"
},
{
"path": "./.nuxt/tsconfig.server.json"
},
{
"path": "./.nuxt/tsconfig.shared.json"
},
{
"path": "./.nuxt/tsconfig.node.json"
}
]
}