From eae8c1f3542656e2be0c7c0f908e238dd13fc61b Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Thu, 9 Jul 2026 01:14:13 +0200 Subject: [PATCH] Implement DDS-005 layout shells --- docs/backlog/initial-build-backlog.md | 72 ++- resources/js/app.tsx | 5 +- resources/js/components/app-header.tsx | 89 +++- resources/js/components/app-logo-icon.tsx | 4 +- resources/js/components/app-logo.tsx | 9 +- resources/js/components/app-sidebar.tsx | 54 ++- resources/js/components/nav-footer.tsx | 33 +- resources/js/layouts/public-layout.tsx | 162 +++++++ resources/js/pages/dashboard.tsx | 178 +++++++- resources/js/pages/welcome.tsx | 518 +++++++--------------- tests/Feature/ExampleTest.php | 14 +- vite.config.ts | 7 +- 12 files changed, 714 insertions(+), 431 deletions(-) create mode 100644 resources/js/layouts/public-layout.tsx diff --git a/docs/backlog/initial-build-backlog.md b/docs/backlog/initial-build-backlog.md index c700d7c..02b9b47 100644 --- a/docs/backlog/initial-build-backlog.md +++ b/docs/backlog/initial-build-backlog.md @@ -261,6 +261,7 @@ Tasks: - create `/events`; - create `/events/{slug}` placeholder; - create `/projects`; +- position `/projects` as a showcase for DDS-built tooling, software, plugins, apps, integrations, and selected community builds; - create `/news`; - create `/locations`; - create `/about`; @@ -273,6 +274,7 @@ Acceptance criteria: - all public routes render; - navigation uses `Events`, `Projects`, `News`, `About`, and `Contact`; - pages can use placeholder content, but no generic theme copy; +- `/projects` placeholder copy clearly frames projects as a public showcase rather than internal project management; - each page has a clear purpose, heading hierarchy, and primary next action. ### DDS-008: Baseline SEO And Redirect Shape @@ -410,6 +412,69 @@ Acceptance criteria: - imported WordPress media can be deduplicated by legacy ID; - alt text is supported. +### DDS-014A: Project Showcase Model + +Goal: model public showcase projects for DDS-built tooling, software, plugins, apps, integrations, and selected community builds. + +Tasks: + +- create `Project` model and migration; +- include title, slug, excerpt, content, status, project_type, visibility, featured flag, and sort order; +- support project types such as `plugin`, `app`, `integration`, `event_tooling`, `community_build`, and `other`; +- add optional links for GitHub, live demo, documentation, download, and contact; +- add optional ownership/credit fields for maintainers or contributors; +- add SEO fields and media references; +- include locale-aware content fields. + +Acceptance criteria: + +- RotorHazard plugins, TrackDraw-style apps, race tooling, livestream overlays, and community utilities can be represented cleanly; +- projects can distinguish open source, public platform, internal/private, and archived work; +- only published and public projects are visible on the public site; +- the model does not imply internal task or project-management workflows; +- project records can be created through factories/tests. + +### DDS-014B: Public Project Showcase Pages + +Goal: show DDS projects publicly as a credible development and tooling showcase. + +Tasks: + +- build `/projects` project index page; +- build `/projects/{slug}` project detail page; +- add filters or grouping by project type when useful; +- show project links, status, media, and concise value proposition; +- add useful empty states. + +Acceptance criteria: + +- `/projects` lists published public projects; +- `/projects/{slug}` shows a published public project; +- project cards communicate type, purpose, status, and primary link clearly; +- archived or internal-only projects are not publicly listed unless explicitly marked visible; +- pages can support both software/tooling projects and selected community builds without separate content types. + +### DDS-014C: Admin Project CRUD + +Goal: manage project showcase content from the `/dashboard` management area. + +Tasks: + +- create `/dashboard/projects` project index; +- create project create/edit forms; +- add server-side validation through Form Requests; +- add project policies; +- add publish status and featured controls; +- make link fields easy to add, remove, and validate. + +Acceptance criteria: + +- admins can create, edit, publish, feature, archive, and reorder projects; +- editors can create and update projects according to their seeded permissions; +- validation errors are shown clearly; +- public visibility follows status and visibility fields; +- forms are structured for concise project showcase editing, not task tracking. + ## Epic 6: WordPress Import Spike ### DDS-015: WordPress Export Discovery @@ -465,7 +530,10 @@ Acceptance criteria: 13. DDS-012 14. DDS-013 15. DDS-014 -16. DDS-015 -17. DDS-016 +16. DDS-014A +17. DDS-014B +18. DDS-014C +19. DDS-015 +20. DDS-016 The public website rebuild can begin once DDS-007 and DDS-010 exist, while admin and import work continue behind it. diff --git a/resources/js/app.tsx b/resources/js/app.tsx index 402353f..d192217 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -4,16 +4,17 @@ import { TooltipProvider } from '@/components/ui/tooltip'; import { initializeTheme } from '@/hooks/use-appearance'; import AppLayout from '@/layouts/app-layout'; import AuthLayout from '@/layouts/auth-layout'; +import PublicLayout from '@/layouts/public-layout'; import SettingsLayout from '@/layouts/settings/layout'; -const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; +const appName = import.meta.env.VITE_APP_NAME || 'Dutch Drone Squad'; createInertiaApp({ title: (title) => (title ? `${title} - ${appName}` : appName), layout: (name) => { switch (true) { case name === 'welcome': - return null; + return PublicLayout; case name.startsWith('auth/'): return AuthLayout; case name.startsWith('settings/'): diff --git a/resources/js/components/app-header.tsx b/resources/js/components/app-header.tsx index 7be23c1..77191e6 100644 --- a/resources/js/components/app-header.tsx +++ b/resources/js/components/app-header.tsx @@ -1,5 +1,16 @@ import { Link, usePage } from '@inertiajs/react'; -import { BookOpen, Folder, LayoutGrid, Menu, Search } from 'lucide-react'; +import { + CalendarDays, + ClipboardList, + Home, + LayoutDashboard, + LifeBuoy, + MapPin, + Menu, + Newspaper, + Search, + ShieldCheck, +} from 'lucide-react'; import AppLogo from '@/components/app-logo'; import AppLogoIcon from '@/components/app-logo-icon'; import { Breadcrumbs } from '@/components/breadcrumbs'; @@ -31,8 +42,8 @@ import { import { UserMenuContent } from '@/components/user-menu-content'; import { useCurrentUrl } from '@/hooks/use-current-url'; import { useInitials } from '@/hooks/use-initials'; -import { cn, toUrl } from '@/lib/utils'; -import { dashboard } from '@/routes'; +import { cn } from '@/lib/utils'; +import { dashboard, home } from '@/routes'; import type { BreadcrumbItem, NavItem } from '@/types'; type Props = { @@ -41,22 +52,47 @@ type Props = { const mainNavItems: NavItem[] = [ { - title: 'Dashboard', + title: 'Overzicht', href: dashboard(), - icon: LayoutGrid, + icon: LayoutDashboard, + }, + { + title: 'Events', + href: `${dashboard.url()}#events`, + icon: CalendarDays, + }, + { + title: 'Projecten', + href: `${dashboard.url()}#projects`, + icon: ClipboardList, + }, + { + title: 'Nieuws', + href: `${dashboard.url()}#news`, + icon: Newspaper, + }, + { + title: 'Locaties', + href: `${dashboard.url()}#locations`, + icon: MapPin, }, ]; const rightNavItems: NavItem[] = [ { - title: 'Repository', - href: 'https://github.com/laravel/react-starter-kit', - icon: Folder, + title: 'Publieke site', + href: home(), + icon: Home, + }, + { + title: 'Huisregels', + href: `${dashboard.url()}#house-rules`, + icon: ShieldCheck, }, { - title: 'Documentation', - href: 'https://laravel.com/docs/starter-kits#react', - icon: BookOpen, + title: 'Support', + href: `${dashboard.url()}#support`, + icon: LifeBuoy, }, ]; @@ -81,6 +117,7 @@ export function AppHeader({ breadcrumbs = [] }: Props) { variant="ghost" size="icon" className="mr-2 h-[34px] w-[34px]" + aria-label="Open navigatiemenu" > @@ -90,10 +127,17 @@ export function AppHeader({ breadcrumbs = [] }: Props) { className="flex h-full w-64 flex-col items-stretch justify-between bg-sidebar" > - Navigation menu + Navigatiemenu - +
+ + + + + Dutch Drone Squad + +
@@ -114,18 +158,16 @@ export function AppHeader({ breadcrumbs = [] }: Props) {
{rightNavItems.map((item) => ( - {item.icon && ( )} {item.title} - + ))}
@@ -182,17 +224,16 @@ export function AppHeader({ breadcrumbs = [] }: Props) { variant="ghost" size="icon" className="group h-9 w-9 cursor-pointer" + aria-label="Zoeken" >
{rightNavItems.map((item) => ( - - + @@ -201,7 +242,7 @@ export function AppHeader({ breadcrumbs = [] }: Props) { {item.icon && ( )} - +

{item.title}

diff --git a/resources/js/components/app-logo-icon.tsx b/resources/js/components/app-logo-icon.tsx index 0c31215..70538e4 100644 --- a/resources/js/components/app-logo-icon.tsx +++ b/resources/js/components/app-logo-icon.tsx @@ -2,11 +2,11 @@ import type { SVGAttributes } from 'react'; export default function AppLogoIcon(props: SVGAttributes) { return ( - + ); diff --git a/resources/js/components/app-logo.tsx b/resources/js/components/app-logo.tsx index e786232..0e6fa92 100644 --- a/resources/js/components/app-logo.tsx +++ b/resources/js/components/app-logo.tsx @@ -3,12 +3,15 @@ import AppLogoIcon from '@/components/app-logo-icon'; export default function AppLogo() { return ( <> -
- +
+
- Laravel Starter Kit + Dutch Drone Squad + + + Platform
diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index a38aa1d..009aebd 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -1,5 +1,14 @@ import { Link } from '@inertiajs/react'; -import { BookOpen, FolderGit2, LayoutGrid } from 'lucide-react'; +import { + CalendarDays, + ClipboardList, + Home, + LayoutDashboard, + LifeBuoy, + MapPin, + Newspaper, + ShieldCheck, +} from 'lucide-react'; import AppLogo from '@/components/app-logo'; import { NavFooter } from '@/components/nav-footer'; import { NavMain } from '@/components/nav-main'; @@ -13,27 +22,52 @@ import { SidebarMenuButton, SidebarMenuItem, } from '@/components/ui/sidebar'; -import { dashboard } from '@/routes'; +import { dashboard, home } from '@/routes'; import type { NavItem } from '@/types'; const mainNavItems: NavItem[] = [ { - title: 'Dashboard', + title: 'Overzicht', href: dashboard(), - icon: LayoutGrid, + icon: LayoutDashboard, + }, + { + title: 'Events', + href: `${dashboard.url()}#events`, + icon: CalendarDays, + }, + { + title: 'Projecten', + href: `${dashboard.url()}#projects`, + icon: ClipboardList, + }, + { + title: 'Nieuws', + href: `${dashboard.url()}#news`, + icon: Newspaper, + }, + { + title: 'Locaties', + href: `${dashboard.url()}#locations`, + icon: MapPin, }, ]; const footerNavItems: NavItem[] = [ { - title: 'Repository', - href: 'https://github.com/laravel/react-starter-kit', - icon: FolderGit2, + title: 'Publieke site', + href: home(), + icon: Home, + }, + { + title: 'Huisregels', + href: `${dashboard.url()}#house-rules`, + icon: ShieldCheck, }, { - title: 'Documentation', - href: 'https://laravel.com/docs/starter-kits#react', - icon: BookOpen, + title: 'Support', + href: `${dashboard.url()}#support`, + icon: LifeBuoy, }, ]; diff --git a/resources/js/components/nav-footer.tsx b/resources/js/components/nav-footer.tsx index 8430e49..d48488c 100644 --- a/resources/js/components/nav-footer.tsx +++ b/resources/js/components/nav-footer.tsx @@ -1,3 +1,4 @@ +import { Link } from '@inertiajs/react'; import type { ComponentPropsWithoutRef } from 'react'; import { SidebarGroup, @@ -16,6 +17,9 @@ export function NavFooter({ }: ComponentPropsWithoutRef & { items: NavItem[]; }) { + const isExternal = (href: NavItem['href']) => + toUrl(href).startsWith('http'); + return ( - - {item.icon && ( - - )} - {item.title} - + {isExternal(item.href) ? ( + + {item.icon && ( + + )} + {item.title} + + ) : ( + + {item.icon && ( + + )} + {item.title} + + )} ))} diff --git a/resources/js/layouts/public-layout.tsx b/resources/js/layouts/public-layout.tsx new file mode 100644 index 0000000..bf14884 --- /dev/null +++ b/resources/js/layouts/public-layout.tsx @@ -0,0 +1,162 @@ +import { Link, usePage } from '@inertiajs/react'; +import type { ReactNode } from 'react'; +import AppLogo from '@/components/app-logo'; +import { cn } from '@/lib/utils'; +import { dashboard, home, login } from '@/routes'; + +type PublicNavItem = { + title: string; + href: string; + current?: boolean; +}; + +const publicNavItems: PublicNavItem[] = [ + { title: 'Events', href: '#events' }, + { title: 'Projecten', href: '#projects' }, + { title: 'Nieuws', href: '#news' }, + { title: 'Over DDS', href: '#about' }, + { title: 'Contact', href: '#contact' }, +]; + +type Props = { + children: ReactNode; +}; + +export default function PublicLayout({ children }: Props) { + const { auth } = usePage().props; + + return ( +
+
+
+ + + + + + +
+ {auth.user ? ( + + Beheer + + ) : ( + + Inloggen + + )} +
+
+ + +
+ +
{children}
+ + +
+ ); +} diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index 3fe0cc7..4d8bd09 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -1,26 +1,172 @@ import { Head } from '@inertiajs/react'; -import { PlaceholderPattern } from '@/components/ui/placeholder-pattern'; +import { + CalendarDays, + ClipboardList, + MapPin, + Newspaper, + ShieldCheck, + Users, +} from 'lucide-react'; import { dashboard } from '@/routes'; +const contentAreas = [ + { + id: 'events', + title: 'Events', + description: 'Publiceer clubdagen, demo-vluchten en trainingsmomenten.', + status: 'Binnenkort uitbreidbaar', + icon: CalendarDays, + }, + { + id: 'projects', + title: 'Projecten', + description: 'Bundel bouwverslagen, missies en community-initiatieven.', + status: 'Klaar voor structuur', + icon: ClipboardList, + }, + { + id: 'news', + title: 'Nieuws', + description: 'Maak updates klaar voor publicatie op de publieke site.', + status: 'Publicatie voorbereid', + icon: Newspaper, + }, + { + id: 'locations', + title: 'Locaties', + description: + 'Bereid vliegplekken, partners en praktische bezoekersinformatie voor.', + status: 'Locaties voorbereid', + icon: MapPin, + }, +]; + +const operations = [ + { + title: 'Toegang', + description: 'Admins en editors beheren de eerste platformcontent.', + icon: ShieldCheck, + }, + { + title: 'Community', + description: + 'Leden, partners en organisatoren krijgen later eigen workflows.', + icon: Users, + }, +]; + export default function Dashboard() { return ( <> - -
-
-
- -
-
- + +
+
+
+
+

+ DDS beheer +

+

+ Platformbasis voor Dutch Drone Squad +

+

+ Gebruik dit startpunt om publieke content, + community-informatie en beheerprocessen verder + op te bouwen. De navigatie toont alvast de + hoofdgebieden van het platform. +

+
+
+ + Fase: + {' '} + + basis ingericht + +
-
- +
+ +
+ {operations.map((item) => ( +
+
+ + + +
+

+ {item.title} +

+

+ {item.description} +

+
+
+
+ ))} +
+ +
+ {contentAreas.map((area) => ( +
+
+ + + + + Binnenkort + +
+

+ {area.title} +

+

+ {area.description} +

+

+ {area.status} +

+
+ ))} +
+ +
+

+ Eerste beheerconventies +

+
+

+ Publieke content krijgt duidelijke titels, + introteksten en vervolgstappen. +

+

+ Beheerpagina's blijven compact, scanbaar en gericht + op herhaald gebruik. +

+

+ Rollen en toegang blijven gekoppeld aan de + admin/editor basis. +

-
-
- -
+
); @@ -29,7 +175,7 @@ export default function Dashboard() { Dashboard.layout = { breadcrumbs: [ { - title: 'Dashboard', + title: 'Beheer', href: dashboard(), }, ], diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx index 3a99fce..f7ec5a3 100644 --- a/resources/js/pages/welcome.tsx +++ b/resources/js/pages/welcome.tsx @@ -1,382 +1,184 @@ import { Head, Link, usePage } from '@inertiajs/react'; +import { + CalendarDays, + CheckCircle2, + ClipboardList, + MapPin, + Newspaper, + Users, +} from 'lucide-react'; import { dashboard, login } from '@/routes'; +const publicSections = [ + { + id: 'events', + title: 'Events', + description: + 'Clubdagen, demo-vluchten en trainingen krijgen hier straks een vaste plek.', + icon: CalendarDays, + }, + { + id: 'projects', + title: 'Projecten', + description: + 'Bouwverslagen, missies en experimenten worden vindbaar voor de community.', + icon: ClipboardList, + }, + { + id: 'news', + title: 'Nieuws', + description: + 'Updates, aankondigingen en terugblikken vormen de publieke tijdlijn.', + icon: Newspaper, + }, + { + id: 'about', + title: 'Community', + description: + 'DDS brengt dronevliegers, makers en partners samen rond veilig vliegen.', + icon: Users, + }, +]; + +const principles = [ + 'Veilig en verantwoord vliegen', + 'Kennis delen binnen de community', + 'Zichtbare activiteiten en projecten', +]; + export default function Welcome() { const { auth } = usePage().props; return ( <> - -
-
- -
-
-
-
-

- Let's get started -

-

- Laravel has an incredibly rich ecosystem. -
- We suggest starting with the following. -

- - + )}
-
- {/* Laravel Logo */} - - - - - - - - - +
- {/* 13 */} - - - - - - - - - - - - +
+
+
+
+

+ Flight board +

+

+ DDS community +

+
+ + Actief + +
- - + {principles.map((principle) => ( +
- - - - - - - - - + + + {principle} + +
+ ))} +
- - - - - - - - - - - +
+
+ +
+

+ Nederland als werkgebied +

+

+ Locaties, partners en huisregels + krijgen hier stap voor stap een + duidelijke plek. +

+
+
+
+
+
+
+ - - - - - - - - - - - +
+
+
+

+ Publieke structuur +

+

+ Eerste navigatiegebieden +

+
- - - - - - - - - - - - -
-
- +
+ {publicSections.map((section) => ( +
+ + + +

+ {section.title} +

+

+ {section.description} +

+
+ ))} +
-
-
+ ); } diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index ef7b533..cd37398 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -1,7 +1,15 @@ get(route('home')); +use Inertia\Testing\AssertableInertia as Assert; - $response->assertOk(); +beforeEach(function () { + $this->withoutVite(); +}); + +test('the public home page renders', function () { + $this->get(route('home')) + ->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->component('welcome'), + ); }); diff --git a/vite.config.ts b/vite.config.ts index eb206f9..c85d0af 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,7 +7,9 @@ import { bunny } from 'laravel-vite-plugin/fonts'; import { defineConfig } from 'vite'; const ddevPrimaryUrl = process.env.DDEV_PRIMARY_URL; -const ddevHostname = ddevPrimaryUrl ? new URL(ddevPrimaryUrl).hostname : undefined; +const ddevHostname = ddevPrimaryUrl + ? new URL(ddevPrimaryUrl).hostname + : undefined; const vitePort = Number(process.env.VITE_PORT ?? 5173); export default defineConfig({ @@ -39,6 +41,9 @@ export default defineConfig({ ...(ddevPrimaryUrl ? { origin: `${ddevPrimaryUrl}:${vitePort}`, + cors: { + origin: [ddevPrimaryUrl], + }, hmr: { host: ddevHostname, protocol: 'wss',