feat(project-crud-sync): add planning artifacts for CRUD project lifecycle #5

Open
NHoarau wants to merge 14 commits from feat/create-projects into main
12 changed files with 284 additions and 65 deletions
Showing only changes of commit a10773f76a - Show all commits

View file

@ -1,20 +1,16 @@
<script setup>
const { t } = useI18n();
useHead({
meta: [{ name: "viewport", content: "width=device-width, initial-scale=1" }],
link: [{ rel: "icon", href: "/favicon.ico" }],
htmlAttrs: {
lang: "en",
},
});
const title = "Work Hub";
const description = "Project management made simple.";
useSeoMeta({
title,
description,
ogTitle: title,
ogDescription: description,
title: () => t("app.title"),
description: () => t("app.description"),
ogTitle: () => t("app.title"),
ogDescription: () => t("app.description"),
twitterCard: "summary_large_image",
});
</script>

View file

@ -3,7 +3,7 @@
<UFooter data-allow-mismatch="children">
NHoarau marked this conversation as resolved
Review

data-allow-mismatch to remove

`data-allow-mismatch` to remove
<template #left>
<p class="text-sm text-muted">Built with Nuxt UI © {{ new Date().getFullYear() }}</p>
<p class="text-sm text-muted">{{ $t('app.title') }} © {{ new Date().getFullYear() }}</p>
</template>
<template #right>

View file

@ -1,3 +1,13 @@
<script setup lang="ts">
import { en, fr } from "@nuxt/ui/locale";
const { locale, setLocale, t } = useI18n();
function handleLocaleChange(value: string) {
setLocale(value as "en" | "fr");
}
</script>
<template>
<UHeader>
<template #left>
@ -5,10 +15,18 @@
<AppLogo class="w-auto h-6 shrink-0" />
</NuxtLink>
<UButton to="/projects" label="Projects" color="neutral" variant="ghost" />
<UButton to="/projects" :label="t('nav.projects')" color="neutral" variant="ghost" />
</template>
<template #right>
<ULocaleSelect
:model-value="locale"
:locales="[en, fr]"
size="sm"
color="neutral"
variant="ghost"
@update:model-value="handleLocaleChange"
/>
<UColorModeButton />
</template>
</UHeader>

View file

@ -1,6 +1,8 @@
<script setup lang="ts">
import { PROJECT_STATUSES, type ProjectStatus } from "@workspace/schema/constants";
const { t } = useI18n();
const props = withDefaults(
defineProps<{
name?: string;
@ -34,21 +36,21 @@ function onSubmit() {
<template>
<form class="space-y-6" @submit.prevent="onSubmit">
Review

Explore use of NuxtUI's UForm

Explore use of NuxtUI's UForm
<UFormField label="Name" required>
<UInput v-model="name" placeholder="Project name" class="w-full" required />
<UFormField :label="t('form.name')" required>
<UInput v-model="name" :placeholder="t('form.namePlaceholder')" class="w-full" required />
</UFormField>
<UFormField label="Description">
<UTextarea v-model="description" placeholder="Optional description" class="w-full" :rows="3" />
<UFormField :label="t('form.description')">
<UTextarea v-model="description" :placeholder="t('form.descriptionPlaceholder')" class="w-full" :rows="3" />
</UFormField>
<UFormField label="Status">
<UFormField :label="t('form.status')">
<USelect v-model="status" :items="PROJECT_STATUSES.map((s) => ({ label: s, value: s }))" class="w-full" />
</UFormField>
<div class="flex gap-3 justify-end">
<UButton label="Cancel" icon="i-lucide-x" color="neutral" variant="outline" to="/projects" />
<UButton type="submit" label="Save" icon="i-lucide-save" color="primary" :loading="saving" />
<UButton :label="t('form.cancel')" icon="i-lucide-x" color="neutral" variant="outline" to="/projects" />
<UButton type="submit" :label="t('form.save')" icon="i-lucide-save" color="primary" :loading="saving" />
</div>
</form>
</template>

View file

@ -1,16 +1,20 @@
<script setup lang="ts">
const { t } = useI18n();
</script>
<template>
<div>
<UPageHero
title="Nuxt Starter Template"
description="A production-ready starter template powered by Nuxt UI. Build beautiful, accessible, and performant applications in minutes, not hours."
:title="t('home.heroTitle')"
:description="t('home.heroDescription')"
:links="[{
label: 'Get started',
label: t('home.getStarted'),
to: 'https://ui.nuxt.com/docs/getting-started/installation/nuxt',
target: '_blank',
trailingIcon: 'i-lucide-arrow-right',
size: 'xl',
}, {
label: 'Use this template',
label: t('home.useTemplate'),
to: 'https://github.com/nuxt-ui-templates/starter',
target: '_blank',
icon: 'i-simple-icons-github',
@ -22,48 +26,48 @@
<UPageSection
id="features"
title="Everything you need to build modern Nuxt apps"
description="Start with a solid foundation. This template includes all the essentials for building production-ready applications with Nuxt UI's powerful component system."
:title="t('home.featuresTitle')"
:description="t('home.featuresDescription')"
:features="[{
icon: 'i-lucide-rocket',
title: 'Production-ready from day one',
description: 'Pre-configured with TypeScript, ESLint, Tailwind CSS, and all the best practices. Focus on building features, not setting up tooling.',
title: t('home.feature1Title'),
description: t('home.feature1Description'),
}, {
icon: 'i-lucide-palette',
title: 'Beautiful by default',
description: 'Leveraging Nuxt UI\'s design system with automatic dark mode, consistent spacing, and polished components that look great out of the box.',
title: t('home.feature2Title'),
description: t('home.feature2Description'),
}, {
icon: 'i-lucide-zap',
title: 'Lightning fast',
description: 'Optimized for performance with SSR/SSG support, automatic code splitting, and edge-ready deployment. Your users will love the speed.',
title: t('home.feature3Title'),
description: t('home.feature3Description'),
}, {
icon: 'i-lucide-blocks',
title: '100+ components included',
description: 'Access Nuxt UI\'s comprehensive component library. From forms to navigation, everything is accessible, responsive, and customizable.',
title: t('home.feature4Title'),
description: t('home.feature4Description'),
}, {
icon: 'i-lucide-code-2',
title: 'Developer experience first',
description: 'Auto-imports, hot module replacement, and TypeScript support. Write less boilerplate and ship more features.',
title: t('home.feature5Title'),
description: t('home.feature5Description'),
}, {
icon: 'i-lucide-shield-check',
title: 'Built for scale',
description: 'Enterprise-ready architecture with proper error handling, SEO optimization, and security best practices built-in.',
title: t('home.feature6Title'),
description: t('home.feature6Description'),
}]"
/>
<UPageSection>
<UPageCTA
title="Ready to build your next Nuxt app?"
description="Join thousands of developers building with Nuxt and Nuxt UI. Get this template and start shipping today."
:title="t('home.ctaTitle')"
:description="t('home.ctaDescription')"
variant="subtle"
:links="[{
label: 'Start building',
label: t('home.startBuilding'),
to: 'https://ui.nuxt.com/docs/getting-started/installation/nuxt',
target: '_blank',
trailingIcon: 'i-lucide-arrow-right',
color: 'neutral',
}, {
label: 'View on GitHub',
label: t('home.viewOnGitHub'),
to: 'https://github.com/nuxt-ui-templates/starter',
target: '_blank',
icon: 'i-simple-icons-github',

View file

@ -1,6 +1,8 @@
<script setup lang="ts">
import type { Project } from "@workspace/schema";
const { t } = useI18n();
definePageMeta({
title: "Edit Project",
});
@ -19,10 +21,10 @@ const apiBase = config.public.apiBaseUrl as string;
onMounted(async () => {
const res = await fetch(`${apiBase}/projects/${route.params.id}`);
if (!res.ok) {
notFound.value = true;
} else {
if (res.ok) {
project.value = await res.json();
} else {
notFound.value = true;
}
loading.value = false;
});
@ -37,14 +39,24 @@ async function handleSubmit(body: { name: string; description?: string; status:
<template>
Review

Not working

Not working
<UPage>
<UPageHeader title="Edit Project" description="Update project details">
<UPageHeader :title="t('projects.edit')" :description="t('projects.editDescription')">
<template #right>
<UButton label="Back" icon="i-lucide-chevron-left" color="neutral" variant="outline" to="/projects" />
<UButton
:label="t('form.back')"
icon="i-lucide-chevron-left"
color="neutral"
variant="outline"
to="/projects" />
</template>
</UPageHeader>
Review

Explore suspense for this async case

Explore suspense for this async case
<UPageBody>
<UAlert v-if="notFound" color="warning" variant="soft" icon="i-lucide-alert-triangle" title="Project not found" />
<UAlert
v-if="notFound"
color="warning"
variant="soft"
icon="i-lucide-alert-triangle"
:title="t('projects.notFound')" />
<UProgress v-else-if="loading" />
@ -54,8 +66,7 @@ async function handleSubmit(body: { name: string; description?: string; status:
:description="project.description ?? undefined"
:status="project.status"
:saving
@submit="handleSubmit"
/>
@submit="handleSubmit" />
</UCard>
</UPageBody>
</UPage>

View file

@ -1,4 +1,5 @@
<script setup lang="ts">
import { computed } from "vue";
import type { TableColumn } from "@nuxt/ui";
import type { Project } from "@workspace/schema";
@ -6,6 +7,22 @@ definePageMeta({
title: "Projects",
});
const { t } = useI18n();
const pageTitle = computed(() => t("projects.title"));
const pageDescription = computed(() => t("projects.description"));
const newProjectLabel = computed(() => t("projects.new"));
const emptyText = computed(() => t("projects.empty"));
const deleteTitle = computed(() => t("projects.delete.title"));
const deleteConfirm = computed(() => t("projects.delete.confirm"));
const deleteCancel = computed(() => t("projects.delete.cancel"));
const deleteDelete = computed(() => t("projects.delete.delete"));
const colName = computed(() => t("projects.columns.name"));
const colStatus = computed(() => t("projects.columns.status"));
const colDescription = computed(() => t("projects.columns.description"));
const colCreated = computed(() => t("projects.columns.created"));
const colActions = computed(() => t("projects.columns.actions"));
const { projects, loading, error, fetchProjects, deleteProject } = useProjectApi();
const deleting = ref<string | null>(null);
@ -28,24 +45,24 @@ async function confirmAndDelete(id: string) {
}
const columns: TableColumn<Project>[] = [
{ accessorKey: "name", header: "Name" },
{ accessorKey: "status", header: "Status" },
{ accessorKey: "name", header: colName.value },
{ accessorKey: "status", header: colStatus.value },
{
accessorKey: "description",
header: "Description",
header: colDescription.value,
cell: ({ row }) => row.getValue("description") ?? "—",
},
{ accessorKey: "createdAt", header: "Created" },
{ id: "actions", header: "Actions" },
{ accessorKey: "createdAt", header: colCreated.value },
{ id: "actions", header: colActions.value },
];
</script>
<template>
<UPage>
<UPageHeader
title="Projects"
description="Manage your projects"
:links="[{ label: 'New project', icon: 'i-lucide-plus', to: '/projects/new', color: 'primary' }]" />
:title="pageTitle"
:description="pageDescription"
:links="[{ label: newProjectLabel, icon: 'i-lucide-plus', to: '/projects/new', color: 'primary' }]" />
<UPageBody>
<UAlert v-if="error" color="error" variant="soft" icon="i-lucide-alert-circle" :title="error" class="mb-4" />
@ -90,7 +107,7 @@ const columns: TableColumn<Project>[] = [
<template #empty>
<div class="py-12 text-center text-muted">
<UIcon name="i-lucide-folder-open" class="size-8 mx-auto mb-2" />
<p>No projects yet.</p>
<p>{{ emptyText }}</p>
</div>
</template>
</UTable>
@ -98,17 +115,29 @@ const columns: TableColumn<Project>[] = [
<UModal v-model:open="confirmDelete">
<template #header>
<h3 class="text-lg font-semibold">Delete project</h3>
<h3 class="text-lg font-semibold">{{ deleteTitle }}</h3>
</template>
<template #body>
<p class="text-sm text-muted">Are you sure you want to delete this project? This action cannot be undone.</p>
<p class="text-sm text-muted">{{ deleteConfirm }}</p>
</template>
<template #footer>
<div class="flex gap-3 justify-end">
<UButton label="Cancel" icon="i-lucide-x" color="neutral" variant="outline" @click="confirmDelete = false; deleting = null" />
<UButton label="Delete" icon="i-lucide-trash-2" color="error" @click="deleting && confirmAndDelete(deleting)" />
<UButton
:label="deleteCancel"
icon="i-lucide-x"
color="neutral"
variant="outline"
@click="
confirmDelete = false;
deleting = null;
" />
<UButton
:label="deleteDelete"
icon="i-lucide-trash-2"
color="error"
@click="deleting && confirmAndDelete(deleting)" />
</div>
</template>
</UModal>

View file

@ -1,4 +1,6 @@
<script setup lang="ts">
const { t } = useI18n();
definePageMeta({
title: "New Project",
});
@ -18,8 +20,8 @@ async function handleSubmit(body: { name: string; description?: string; status:
<template>
<UPage>
<UPageHeader
title="New Project"
description="Create a new project"
:title="t('projects.create')"
:description="t('projects.createDescription')"
/>
<UPageBody>

View file

@ -0,0 +1,7 @@
export default defineI18nConfig(() => ({
legacy: false,
locale: "en",
fallbackLocale: "en",
silentFallbackWarn: true,
silentTranslationWarn: true,
}));

View file

@ -0,0 +1,67 @@
{
"app": {
"title": "Work Hub",
"description": "Project management made simple."
},
"nav": {
"projects": "Projects"
},
"projects": {
"title": "Projects",
"description": "Manage your projects",
"new": "New project",
"edit": "Edit Project",
"editDescription": "Update project details",
"create": "New Project",
"createDescription": "Create a new project",
"columns": {
"name": "Name",
"status": "Status",
"description": "Description",
"created": "Created",
"actions": "Actions"
},
"delete": {
"title": "Delete project",
"confirm": "Are you sure you want to delete this project? This action cannot be undone.",
"cancel": "Cancel",
"delete": "Delete"
},
"notFound": "Project not found",
"empty": "No projects yet."
},
"form": {
"name": "Name",
"namePlaceholder": "Project name",
"description": "Description",
"descriptionPlaceholder": "Optional description",
"status": "Status",
"save": "Save",
"cancel": "Cancel",
"back": "Back"
},
"home": {
"heroTitle": "Nuxt Starter Template",
"heroDescription": "A production-ready starter template powered by Nuxt UI. Build beautiful, accessible, and performant applications in minutes, not hours.",
"getStarted": "Get started",
"useTemplate": "Use this template",
"featuresTitle": "Everything you need to build modern Nuxt apps",
"featuresDescription": "Start with a solid foundation. This template includes all the essentials for building production-ready applications with Nuxt UI's powerful component system.",
"feature1Title": "Production-ready from day one",
"feature1Description": "Pre-configured with TypeScript, ESLint, Tailwind CSS, and all the best practices. Focus on building features, not setting up tooling.",
"feature2Title": "Beautiful by default",
"feature2Description": "Leveraging Nuxt UI's design system with automatic dark mode, consistent spacing, and polished components that look great out of the box.",
"feature3Title": "Lightning fast",
"feature3Description": "Optimized for performance with SSR/SSG support, automatic code splitting, and edge-ready deployment. Your users will love the speed.",
"feature4Title": "100+ components included",
"feature4Description": "Access Nuxt UI's comprehensive component library. From forms to navigation, everything is accessible, responsive, and customizable.",
"feature5Title": "Developer experience first",
"feature5Description": "Auto-imports, hot module replacement, and TypeScript support. Write less boilerplate and ship more features.",
"feature6Title": "Built for scale",
"feature6Description": "Enterprise-ready architecture with proper error handling, SEO optimization, and security best practices built-in.",
"ctaTitle": "Ready to build your next Nuxt app?",
"ctaDescription": "Join thousands of developers building with Nuxt and Nuxt UI. Get this template and start shipping today.",
"startBuilding": "Start building",
"viewOnGitHub": "View on GitHub"
}
}

View file

@ -0,0 +1,67 @@
{
"app": {
"title": "Work Hub",
"description": "La gestion de projets simplifiée."
},
"nav": {
"projects": "Projets"
},
"projects": {
"title": "Projets",
"description": "Gérez vos projets",
"new": "Nouveau projet",
"edit": "Modifier le projet",
"editDescription": "Mettre à jour les détails du projet",
"create": "Nouveau projet",
"createDescription": "Créer un nouveau projet",
"columns": {
"name": "Nom",
"status": "Statut",
"description": "Description",
"created": "Créé le",
"actions": "Actions"
},
"delete": {
"title": "Supprimer le projet",
"confirm": "Êtes-vous sûr de vouloir supprimer ce projet ? Cette action est irréversible.",
"cancel": "Annuler",
"delete": "Supprimer"
},
"notFound": "Projet introuvable",
"empty": "Aucun projet pour le moment."
},
"form": {
"name": "Nom",
"namePlaceholder": "Nom du projet",
"description": "Description",
"descriptionPlaceholder": "Description facultative",
"status": "Statut",
"save": "Enregistrer",
"cancel": "Annuler",
"back": "Retour"
},
"home": {
"heroTitle": "Nuxt Starter Template",
"heroDescription": "Un modèle de démarrage prêt pour la production propulsé par Nuxt UI. Créez des applications belles, accessibles et performantes en quelques minutes.",
"getStarted": "Commencer",
"useTemplate": "Utiliser ce modèle",
"featuresTitle": "Tout ce dont vous avez besoin pour créer des apps Nuxt modernes",
"featuresDescription": "Partez sur des bases solides. Ce modèle inclut tous les essentiels pour créer des applications prêtes pour la production avec le système de composants puissant de Nuxt UI.",
"feature1Title": "Prêt pour la production dès le premier jour",
"feature1Description": "Préconfiguré avec TypeScript, ESLint, Tailwind CSS et toutes les bonnes pratiques. Concentrez-vous sur les fonctionnalités, pas sur la configuration.",
"feature2Title": "Beau par défaut",
"feature2Description": "Tirez parti du système de design de Nuxt UI avec le mode sombre automatique, une espacement cohérent et des composants soignés qui sont superbes dès la sortie de la boîte.",
"feature3Title": "Rapide comme l'éclair",
"feature3Description": "Optimisé pour la performance avec le support SSR/SSG, la séparation automatique du code et un déploiement edge-ready. Vos utilisateurs adoreront la vitesse.",
"feature4Title": "100+ composants inclus",
"feature4Description": "Accédez à la bibliothèque complète de composants de Nuxt UI. Des formulaires à la navigation, tout est accessible, responsive et personnalisable.",
"feature5Title": "L'expérience développeur avant tout",
"feature5Description": "Auto-imports, remplacement de modules à chaud et support TypeScript. Écrivez moins de code passe-partout et livrez plus de fonctionnalités.",
"feature6Title": "Conçu pour passer à l'échelle",
"feature6Description": "Architecture prête pour l'entreprise avec une gestion des erreurs appropriée, une optimisation SEO et des bonnes pratiques de sécurité intégrées.",
"ctaTitle": "Prêt à créer votre prochaine app Nuxt ?",
"ctaDescription": "Rejoignez des milliers de développeurs qui construisent avec Nuxt et Nuxt UI. Obtenez ce modèle et commencez à livrer dès aujourd'hui.",
"startBuilding": "Commencer à développer",
"viewOnGitHub": "Voir sur GitHub"
}
}

View file

@ -1,6 +1,6 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
modules: ["@nuxt/eslint", "@nuxt/ui"],
modules: ["@nuxt/eslint", "@nuxt/ui", "@nuxtjs/i18n"],
devtools: {
enabled: true,
@ -24,4 +24,20 @@ export default defineNuxtConfig({
},
},
},
i18n: {
locales: [
{ code: "en", language: "en", name: "English", file: "en.json" },
{ code: "fr", language: "fr", name: "Français", file: "fr.json" },
],
defaultLocale: "en",
strategy: "no_prefix",
langDir: "locales",
detectBrowserLanguage: {
useCookie: true,
cookieKey: "i18n_redirected",
redirectOn: "root",
},
vueI18n: "i18n.config.ts",
},
});