Phase 1 UI system

This commit is contained in:
Codex
2026-07-10 12:56:04 -05:00
parent 2ee16a6469
commit b45a9eccd6
30 changed files with 324 additions and 292 deletions
+3
View File
@@ -8,6 +8,9 @@ This project uses Git as the primary rollback trail. The `.old` convention is re
### Added
- Added a Phase 1 UI system pass with dark/light theme support, shared page/action/stat components, a product-focused home page, and a more useful dashboard.
- Added roadmap guardrails for open-source-friendly dependencies, public themes, and future license audits.
- Documented `pdo_sqlite` and `sqlite3` as required PHP extensions for SQLite-backed local tests and first-pass deployments.
- Created the Laravel Provider Exchange prototype with Breeze authentication, seeded demo users, and role support for admins, providers, buyers, and hybrid accounts.
- Added provider and buyer profile management with service areas, profile details, and external profile snapshot records.
- Added social posts, buyer job posts, provider quotes, quote revisions, quote decline flow, and buyer quote acceptance.
+7 -1
View File
@@ -17,7 +17,7 @@ This document records the explicit runtime requirements and server changes for P
- `mbstring`
- `openssl`
- `pdo`
- `pdo_sqlite` for the first live test deployment
- `pdo_sqlite` for SQLite test deployments and the default automated test suite
- `session`
- `tokenizer`
- `xml`
@@ -28,6 +28,12 @@ This document records the explicit runtime requirements and server changes for P
- `bootstrap/cache/`
- the SQLite database file for SQLite test deployments.
## Local Test Requirements
- The default PHPUnit configuration uses an in-memory SQLite database, so whichever PHP runtime executes `php artisan test` must include `pdo_sqlite` and `sqlite3`.
- On Windows, `php -m` should list both `PDO` and `pdo_sqlite`; otherwise the feature tests fail before reaching app code with `could not find driver`.
- WSL Ubuntu is acceptable for local verification when Windows PHP is missing SQLite support, as long as WSL PHP has `pdo_sqlite` and `sqlite3`.
## Recommended Production Runtime
- Nginx.
+1
View File
@@ -31,6 +31,7 @@ APP_DIR=/var/www/technical-services-exchange bash /var/www/technical-services-ex
## Important Notes
- The installer is a first-pass Ubuntu/Nginx helper, not a universal hosting panel installer.
- Local and CI test runners need PHP SQLite support: `pdo_sqlite` and `sqlite3`.
- Review `.env` before production use.
- Configure real mail before inviting users.
- Move sessions, cache, and queues to Redis before serious traffic.
+3 -1
View File
@@ -29,12 +29,13 @@ Provider Exchange is a Laravel application for a provider-centered field-service
- Payment remains direct between buyer and provider unless a compliant payment partner is deliberately added later.
- Reputation should be mutual, transparent, and appealable through community review.
- Dispute voting is a community reputation signal, not legal adjudication.
- Runtime dependencies, UI components, and themes should stay open-source friendly. Avoid proprietary theme kits, unclear asset licenses, or dependencies that would restrict commercial, nonprofit, or community scaling.
## Next Build Phases
1. Profile depth: richer galleries, equipment lists, certifications, insurance fields, service coverage maps, and imported review summaries.
2. Directory quality: saved searches, better filtering, profile badges, availability signals, and stronger buyer/provider discovery pages.
3. Work-order depth: checklist templates, required evidence rules, appointment windows, onsite timestamps, and change-request records.
3. Work-order depth: checklist templates, required evidence rules, appointment windows, onsite timestamps, change-request records, and print/PDF export for work-order packets.
4. Reputation refinement: rating category definitions, anti-abuse controls, edit windows, report flows, and moderation views.
5. Dispute workflow: better evidence timelines, reason-coded votes, quorum/visibility rules, and admin/community moderation tools.
6. Notifications: channel preferences, email templates, and event-specific subscription controls.
@@ -42,6 +43,7 @@ Provider Exchange is a Laravel application for a provider-centered field-service
8. Import tooling: guided manual imports for Field Nation, WorkMarket, and similar profile/review history.
9. Admin operations: audit logs, queue health, system status, content moderation, and backup/export workflows.
10. Deployment hardening: production environment docs, queue worker setup, scheduler setup, storage policy, and server provisioning notes.
11. License and runtime audit: generate and review PHP and JavaScript dependency license reports before adding external theme kits, map any risky package to a replacement, and keep required local/runtime extensions such as `pdo_sqlite` documented for testability.
## Scaling Risks To Track
+28
View File
@@ -1,3 +1,31 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
color-scheme: light;
}
html.dark {
color-scheme: dark;
}
body {
@apply bg-slate-100 text-slate-950 antialiased dark:bg-slate-950 dark:text-slate-100;
}
[x-cloak] {
display: none !important;
}
}
@layer components {
.tse-panel {
@apply rounded-lg border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900;
}
.tse-muted {
@apply text-slate-600 dark:text-slate-400;
}
}
+3 -3
View File
@@ -27,14 +27,14 @@
<!-- Remember Me -->
<div class="block mt-4">
<label for="remember_me" class="inline-flex items-center">
<input id="remember_me" type="checkbox" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500" name="remember">
<span class="ms-2 text-sm text-gray-600">{{ __('Remember me') }}</span>
<input id="remember_me" type="checkbox" class="rounded border-slate-300 text-sky-600 shadow-sm focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950" name="remember">
<span class="ms-2 text-sm text-slate-600 dark:text-slate-400">{{ __('Remember me') }}</span>
</label>
</div>
<div class="flex items-center justify-end mt-4">
@if (Route::has('password.request'))
<a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('password.request') }}">
<a class="rounded-md text-sm text-slate-600 underline hover:text-slate-950 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 dark:text-slate-400 dark:hover:text-white dark:focus:ring-offset-slate-900" href="{{ route('password.request') }}">
{{ __('Forgot your password?') }}
</a>
@endif
+2 -2
View File
@@ -18,7 +18,7 @@
<div class="mt-4">
<x-input-label for="account_type" :value="__('Account type')" />
<select id="account_type" name="account_type" required class="block mt-1 w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm">
<select id="account_type" name="account_type" required class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
<option value="provider" @selected(old('account_type') === 'provider')>Provider</option>
<option value="buyer" @selected(old('account_type') === 'buyer')>Buyer</option>
<option value="both" @selected(old('account_type') === 'both')>Provider and buyer</option>
@@ -50,7 +50,7 @@
</div>
<div class="flex items-center justify-end mt-4">
<a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('login') }}">
<a class="rounded-md text-sm text-slate-600 underline hover:text-slate-950 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 dark:text-slate-400 dark:hover:text-white dark:focus:ring-offset-slate-900" href="{{ route('login') }}">
{{ __('Already registered?') }}
</a>
@@ -0,0 +1,12 @@
@props(['title', 'description', 'href' => null, 'label' => 'Open'])
<div class="tse-panel flex h-full flex-col p-5">
<h3 class="text-base font-semibold text-slate-950 dark:text-white">{{ $title }}</h3>
<p class="mt-2 flex-1 text-sm leading-6 text-slate-600 dark:text-slate-400">{{ $description }}</p>
@if ($href)
<a href="{{ $href }}" class="mt-4 inline-flex items-center text-sm font-semibold text-sky-700 hover:text-sky-900 dark:text-sky-300 dark:hover:text-sky-200">
{{ $label }}
<span aria-hidden="true" class="ms-1">-&gt;</span>
</a>
@endif
</div>
@@ -0,0 +1,15 @@
@props(['tone' => 'slate'])
@php
$tones = [
'sky' => 'bg-sky-100 text-sky-800 dark:bg-sky-950 dark:text-sky-200',
'emerald' => 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-200',
'amber' => 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-200',
'rose' => 'bg-rose-100 text-rose-800 dark:bg-rose-950 dark:text-rose-200',
'slate' => 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-200',
];
@endphp
<span {{ $attributes->merge(['class' => 'inline-flex items-center rounded-full px-2.5 py-1 text-xs font-semibold '.$tones[$tone]]) }}>
{{ $slot }}
</span>
@@ -1,3 +1,3 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-xs font-semibold uppercase tracking-widest text-white transition duration-150 ease-in-out hover:bg-red-500 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 active:bg-red-700 dark:focus:ring-offset-slate-900']) }}>
{{ $slot }}
</button>
@@ -1 +1 @@
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-slate-700 transition duration-150 ease-in-out hover:bg-slate-100 focus:bg-slate-100 focus:outline-none dark:text-slate-200 dark:hover:bg-slate-800 dark:focus:bg-slate-800']) }}>{{ $slot }}</a>
@@ -1,4 +1,4 @@
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white'])
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-slate-900'])
@php
$alignmentClasses = match ($align) {
@@ -28,7 +28,7 @@ $width = match ($width) {
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
style="display: none;"
@click="open = false">
<div class="rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}">
<div class="rounded-md ring-1 ring-black ring-opacity-5 dark:ring-white/10 {{ $contentClasses }}">
{{ $content }}
</div>
</div>
+7 -4
View File
@@ -1,11 +1,14 @@
@props(['name', 'label', 'value' => '', 'textarea' => false, 'type' => 'text'])
@props(['name', 'label', 'value' => '', 'textarea' => false, 'type' => 'text', 'help' => null])
<div>
<label for="{{ $name }}" class="block text-sm font-medium text-gray-700">{{ $label }}</label>
<label for="{{ $name }}" class="block text-sm font-medium text-slate-800 dark:text-slate-200">{{ $label }}</label>
@if ($textarea)
<textarea id="{{ $name }}" name="{{ $name }}" rows="4" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old($name, $value) }}</textarea>
<textarea id="{{ $name }}" name="{{ $name }}" rows="4" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">{{ old($name, $value) }}</textarea>
@else
<input id="{{ $name }}" type="{{ $type }}" name="{{ $name }}" value="{{ old($name, $value) }}" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<input id="{{ $name }}" type="{{ $type }}" name="{{ $name }}" value="{{ old($name, $value) }}" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
@endif
@if ($help)
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">{{ $help }}</p>
@endif
@error($name)
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@@ -1,7 +1,7 @@
@props(['messages'])
@if ($messages)
<ul {{ $attributes->merge(['class' => 'text-sm text-red-600 space-y-1']) }}>
<ul {{ $attributes->merge(['class' => 'space-y-1 text-sm text-red-600 dark:text-red-400']) }}>
@foreach ((array) $messages as $message)
<li>{{ $message }}</li>
@endforeach
@@ -1,5 +1,5 @@
@props(['value'])
<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700']) }}>
<label {{ $attributes->merge(['class' => 'block text-sm font-medium text-slate-800 dark:text-slate-200']) }}>
{{ $value ?? $slot }}
</label>
@@ -2,8 +2,8 @@
@php
$classes = ($active ?? false)
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out';
? 'inline-flex items-center px-1 pt-1 border-b-2 border-sky-500 text-sm font-medium leading-5 text-slate-950 focus:outline-none focus:border-sky-700 transition duration-150 ease-in-out dark:text-white'
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-slate-500 hover:text-slate-800 hover:border-slate-300 focus:outline-none focus:text-slate-800 focus:border-slate-300 transition duration-150 ease-in-out dark:text-slate-400 dark:hover:text-slate-100 dark:hover:border-slate-600 dark:focus:text-slate-100 dark:focus:border-slate-600';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>
@@ -0,0 +1,19 @@
@props(['eyebrow' => null, 'title', 'description' => null])
<div class="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
<div>
@if ($eyebrow)
<p class="text-sm font-semibold uppercase tracking-wide text-sky-700 dark:text-sky-300">{{ $eyebrow }}</p>
@endif
<h1 class="text-2xl font-semibold tracking-tight text-slate-950 dark:text-white">{{ $title }}</h1>
@if ($description)
<p class="mt-2 max-w-3xl text-sm leading-6 text-slate-600 dark:text-slate-400">{{ $description }}</p>
@endif
</div>
@if (trim($slot) !== '')
<div class="flex flex-wrap items-center gap-2">
{{ $slot }}
</div>
@endif
</div>
@@ -1,3 +1,3 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center rounded-md border border-transparent bg-sky-600 px-4 py-2 text-xs font-semibold uppercase tracking-widest text-white transition duration-150 ease-in-out hover:bg-sky-700 focus:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 active:bg-sky-800 dark:focus:ring-offset-slate-900']) }}>
{{ $slot }}
</button>
@@ -2,8 +2,8 @@
@php
$classes = ($active ?? false)
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 text-start text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out'
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out';
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-sky-500 text-start text-base font-medium text-sky-800 bg-sky-50 focus:outline-none focus:text-sky-900 focus:bg-sky-100 focus:border-sky-700 transition duration-150 ease-in-out dark:bg-sky-950/40 dark:text-sky-200'
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-slate-600 hover:text-slate-900 hover:bg-slate-50 hover:border-slate-300 focus:outline-none focus:text-slate-900 focus:bg-slate-50 focus:border-slate-300 transition duration-150 ease-in-out dark:text-slate-300 dark:hover:text-white dark:hover:bg-slate-800 dark:hover:border-slate-600 dark:focus:text-white dark:focus:bg-slate-800 dark:focus:border-slate-600';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>
@@ -1,3 +1,3 @@
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 transition ease-in-out duration-150']) }}>
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center rounded-md border border-slate-300 bg-white px-4 py-2 text-xs font-semibold uppercase tracking-widest text-slate-700 shadow-sm transition duration-150 ease-in-out hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 disabled:opacity-25 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus:ring-offset-slate-900']) }}>
{{ $slot }}
</button>
@@ -0,0 +1,9 @@
@props(['label', 'value', 'description' => null])
<div class="tse-panel p-5">
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">{{ $label }}</p>
<p class="mt-2 text-3xl font-semibold tracking-tight text-slate-950 dark:text-white">{{ $value }}</p>
@if ($description)
<p class="mt-2 text-sm text-slate-600 dark:text-slate-400">{{ $description }}</p>
@endif
</div>
@@ -1,3 +1,3 @@
@props(['disabled' => false])
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) }}>
<input @disabled($disabled) {{ $attributes->merge(['class' => 'rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100']) }}>
@@ -0,0 +1,21 @@
<button
type="button"
x-data="{
dark: document.documentElement.classList.contains('dark'),
toggle() {
this.dark = ! this.dark;
document.documentElement.classList.toggle('dark', this.dark);
localStorage.setItem('tse-theme', this.dark ? 'dark' : 'light');
}
}"
x-on:click="toggle()"
class="inline-flex h-9 w-9 items-center justify-center rounded-md border border-slate-200 bg-white text-slate-600 shadow-sm transition hover:border-slate-300 hover:text-slate-950 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:border-slate-600 dark:hover:text-white dark:focus:ring-offset-slate-900"
aria-label="Toggle color theme"
>
<svg x-show="!dark" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M10 2a1 1 0 0 1 1 1v1a1 1 0 1 1-2 0V3a1 1 0 0 1 1-1Zm0 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm7-5a1 1 0 1 1 0 2h-1a1 1 0 1 1 0-2h1ZM5 10a1 1 0 0 1-1 1H3a1 1 0 1 1 0-2h1a1 1 0 0 1 1 1Zm10.657-5.657a1 1 0 0 1 0 1.414l-.707.707a1 1 0 1 1-1.414-1.414l.707-.707a1 1 0 0 1 1.414 0ZM6.464 13.536a1 1 0 0 1 0 1.414l-.707.707a1 1 0 0 1-1.414-1.414l.707-.707a1 1 0 0 1 1.414 0Zm9.193 2.121a1 1 0 0 1-1.414 0l-.707-.707a1 1 0 0 1 1.414-1.414l.707.707a1 1 0 0 1 0 1.414ZM6.464 6.464A1 1 0 0 1 5.05 6.464l-.707-.707a1 1 0 0 1 1.414-1.414l.707.707a1 1 0 0 1 0 1.414ZM10 15a1 1 0 0 1 1 1v1a1 1 0 1 1-2 0v-1a1 1 0 0 1 1-1Z"/>
</svg>
<svg x-show="dark" x-cloak class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M7.455 2.004a.75.75 0 0 1 .79.927 7.5 7.5 0 0 0 8.824 8.824.75.75 0 0 1 .927.79A8.5 8.5 0 1 1 7.455 2.004Z" clip-rule="evenodd"/>
</svg>
</button>
+45 -37
View File
@@ -1,52 +1,60 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</h2>
<x-page-header
title="Dashboard"
description="A quick operating view for profiles, jobs, work orders, and reputation activity."
>
<a href="{{ route('jobs.create') }}" class="rounded-md bg-sky-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-700">Post job</a>
</x-page-header>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 space-y-4">
<div>
<p class="text-sm text-gray-500">Signed in as</p>
<p class="font-semibold">{{ auth()->user()->name }}</p>
<div class="py-8">
<div class="mx-auto max-w-7xl space-y-6 px-4 sm:px-6 lg:px-8">
<section class="grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
<x-stat-card label="Providers" :value="$counts['providers']" description="Service profiles" />
<x-stat-card label="Buyers" :value="$counts['buyers']" description="Company profiles" />
<x-stat-card label="Jobs" :value="$counts['jobs']" description="Open and historical posts" />
<x-stat-card label="Work orders" :value="$counts['workOrders']" description="Accepted quote records" />
<x-stat-card label="Disputes" :value="$counts['disputes']" description="Peer review cases" />
</section>
<section class="grid gap-4 lg:grid-cols-[.8fr_1.2fr]">
<div class="tse-panel p-5">
<h2 class="text-base font-semibold text-slate-950 dark:text-white">Signed in</h2>
<div class="mt-4">
<p class="text-sm text-slate-500 dark:text-slate-400">Account</p>
<p class="font-semibold text-slate-950 dark:text-white">{{ auth()->user()->name }}</p>
<p class="text-sm text-slate-600 dark:text-slate-400">{{ auth()->user()->email }}</p>
</div>
<div>
<p class="text-sm text-gray-500">Roles</p>
<div class="mt-5">
<p class="text-sm text-slate-500 dark:text-slate-400">Roles</p>
<div class="mt-2 flex flex-wrap gap-2">
@foreach (auth()->user()->roles->pluck('name') as $role)
<span class="rounded bg-gray-100 px-3 py-1 text-sm">{{ $role }}</span>
<x-badge tone="slate">{{ $role }}</x-badge>
@endforeach
</div>
</div>
<div class="grid gap-4 md:grid-cols-3">
@role('provider')
<div class="rounded border border-gray-200 p-4">
<h3 class="font-semibold">Provider workspace</h3>
<p class="mt-2 text-sm text-gray-600">Profile, service areas, rate cards, and work requests will land here.</p>
</div>
@endrole
@role('buyer')
<div class="rounded border border-gray-200 p-4">
<h3 class="font-semibold">Buyer workspace</h3>
<p class="mt-2 text-sm text-gray-600">Company profile, job posts, quotes, and assignments will land here.</p>
</div>
@endrole
@role('admin')
<div class="rounded border border-gray-200 p-4">
<h3 class="font-semibold">Admin workspace</h3>
<p class="mt-2 text-sm text-gray-600">Moderation, role review, and platform controls will land here.</p>
</div>
@endrole
</div>
</div>
</div>
<div class="grid gap-4 md:grid-cols-3">
@role('provider')
<x-action-card title="Provider workspace" description="Update services, coverage, equipment, rate cards, and imported profile history." :href="route('providers.edit')" label="Edit profile" />
@endrole
@role('buyer')
<x-action-card title="Buyer workspace" description="Maintain company details, create jobs, review quotes, and manage accepted work." :href="route('buyers.edit')" label="Edit profile" />
@endrole
@role('admin')
<x-action-card title="Admin workspace" description="Review users, moderation surfaces, ratings, disputes, and platform operating signals." :href="route('admin.index')" label="Open admin" />
@endrole
<x-action-card title="Notifications" description="Review unread platform events for quotes, work orders, reviews, disputes, and profile activity." :href="route('notifications.index')" label="Open inbox" />
<x-action-card title="Directory" description="Search providers and buyers without enforcing platform-set rates or preferred pricing." :href="route('providers.index')" label="Browse" />
<x-action-card title="Jobs" description="Review posted opportunities, quote history, and accepted work-order paths." :href="route('jobs.index')" label="View jobs" />
</div>
</section>
</div>
</div>
</x-app-layout>
+12 -3
View File
@@ -12,15 +12,24 @@
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
<script>
(function () {
const stored = localStorage.getItem('tse-theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (stored === 'dark' || (!stored && prefersDark)) {
document.documentElement.classList.add('dark');
}
})();
</script>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100">
<body class="font-sans">
<div class="min-h-screen bg-slate-100 dark:bg-slate-950">
@include('layouts.navigation')
<!-- Page Heading -->
@isset($header)
<header class="bg-white shadow">
<header class="border-b border-slate-200 bg-white/90 shadow-sm dark:border-slate-800 dark:bg-slate-900/90">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{{ $header }}
</div>
+17 -6
View File
@@ -12,17 +12,28 @@
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
<script>
(function () {
const stored = localStorage.getItem('tse-theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (stored === 'dark' || (!stored && prefersDark)) {
document.documentElement.classList.add('dark');
}
})();
</script>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans text-gray-900 antialiased">
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
<div>
<a href="/">
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
<body class="font-sans">
<div class="min-h-screen flex flex-col sm:justify-center items-center px-4 pt-6 sm:pt-0 bg-slate-100 dark:bg-slate-950">
<div class="flex items-center gap-3">
<a href="{{ url('/') }}" class="flex items-center gap-3 text-slate-950 dark:text-white">
<x-application-logo class="h-10 w-10 fill-current text-sky-600 dark:text-sky-400" />
<span class="text-lg font-semibold">Provider Exchange</span>
</a>
<x-theme-toggle />
</div>
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
<div class="w-full sm:max-w-md mt-6 overflow-hidden rounded-lg border border-slate-200 bg-white px-6 py-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
{{ $slot }}
</div>
</div>
+14 -11
View File
@@ -1,12 +1,13 @@
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
<nav x-data="{ open: false }" class="border-b border-slate-200 bg-white/95 dark:border-slate-800 dark:bg-slate-900/95">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<a href="{{ route('dashboard') }}">
<x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
<a href="{{ route('dashboard') }}" class="flex items-center gap-2 text-slate-950 dark:text-white">
<x-application-logo class="block h-9 w-auto fill-current text-sky-600 dark:text-sky-400" />
<span class="hidden text-sm font-semibold tracking-wide md:inline">Provider Exchange</span>
</a>
</div>
@@ -47,11 +48,12 @@
</div>
<!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ms-6">
<div class="hidden sm:flex sm:items-center sm:ms-6 sm:gap-3">
<x-theme-toggle />
@auth
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
<button class="inline-flex items-center rounded-md border border-transparent bg-white px-3 py-2 text-sm font-medium leading-4 text-slate-600 transition duration-150 ease-in-out hover:text-slate-900 focus:outline-none dark:bg-slate-900 dark:text-slate-300 dark:hover:text-white">
<div>{{ Auth::user()->name }}</div>
<div class="ms-1">
@@ -91,15 +93,16 @@
</x-dropdown>
@else
<div class="flex gap-4 text-sm">
<a href="{{ route('login') }}" class="text-gray-600 hover:text-gray-900">Log in</a>
<a href="{{ route('register') }}" class="text-gray-600 hover:text-gray-900">Register</a>
<a href="{{ route('login') }}" class="text-slate-600 hover:text-slate-950 dark:text-slate-300 dark:hover:text-white">Log in</a>
<a href="{{ route('register') }}" class="text-slate-600 hover:text-slate-950 dark:text-slate-300 dark:hover:text-white">Register</a>
</div>
@endauth
</div>
<!-- Hamburger -->
<div class="-me-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
<x-theme-toggle />
<button @click="open = ! open" class="ms-2 inline-flex items-center justify-center rounded-md p-2 text-slate-500 transition duration-150 ease-in-out hover:bg-slate-100 hover:text-slate-700 focus:bg-slate-100 focus:text-slate-700 focus:outline-none dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white dark:focus:bg-slate-800 dark:focus:text-white">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
@@ -145,11 +148,11 @@
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200">
<div class="pt-4 pb-1 border-t border-slate-200 dark:border-slate-800">
@auth
<div class="px-4">
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
<div class="font-medium text-base text-slate-900 dark:text-white">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-slate-500 dark:text-slate-400">{{ Auth::user()->email }}</div>
</div>
@endauth
File diff suppressed because one or more lines are too long
+14 -1
View File
@@ -17,6 +17,11 @@ use App\Http\Controllers\ReviewController;
use App\Http\Controllers\SocialPostController;
use App\Http\Controllers\WorkOrderMessageController;
use App\Http\Controllers\WorkOrderController;
use App\Models\BuyerProfile;
use App\Models\Dispute;
use App\Models\JobPost;
use App\Models\ProviderProfile;
use App\Models\WorkOrder;
use Illuminate\Support\Facades\Route;
$basePath = trim((string) config('app.base_path', ''), '/');
@@ -31,7 +36,15 @@ Route::get('/terms', function () {
})->name('terms');
Route::get('/dashboard', function () {
return view('dashboard');
return view('dashboard', [
'counts' => [
'providers' => ProviderProfile::count(),
'buyers' => BuyerProfile::count(),
'jobs' => JobPost::count(),
'workOrders' => WorkOrder::count(),
'disputes' => Dispute::count(),
],
]);
})->middleware(['auth', 'verified'])->name('dashboard');
Route::middleware('auth')->group(function () {
+2
View File
@@ -3,6 +3,8 @@ import forms from '@tailwindcss/forms';
/** @type {import('tailwindcss').Config} */
export default {
darkMode: 'class',
content: [
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
'./storage/framework/views/*.php',