Calendar now grabs all events and formatted date ranges for display; calendar view shows mini calendar; more theme and brand updates for better glitz
@ -2,36 +2,99 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\Calendar;
|
||||
use App\Models\CalendarMeta;
|
||||
use App\Models\CalendarInstance;
|
||||
use App\Models\Event;
|
||||
|
||||
class CalendarController extends Controller
|
||||
{
|
||||
/**
|
||||
* list calendars owned by the logged-in user
|
||||
* Consolidated calendar dashboard.
|
||||
*
|
||||
* Query params:
|
||||
* view = month | week | 4day (default: month)
|
||||
* date = Y-m-d anchor date (default: today, in user TZ)
|
||||
*
|
||||
* The view receives a `$payload` array:
|
||||
* ├─ view current view name
|
||||
* ├─ range ['start' => Carbon, 'end' => Carbon]
|
||||
* ├─ calendars keyed by calendar id (for the left-hand toggle list)
|
||||
* └─ events flat list of VEVENTs in that range
|
||||
*/
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
// set the calendar key
|
||||
$principal = auth()->user()->principal_uri;
|
||||
|
||||
// get the view and time range
|
||||
[$view, $range] = $this->resolveRange($request);
|
||||
|
||||
// load the user's calendars
|
||||
$calendars = Calendar::query()
|
||||
->select('calendars.*', 'ci.displayname as instance_displayname') // ← add
|
||||
->select(
|
||||
'calendars.id',
|
||||
'ci.displayname',
|
||||
'ci.calendarcolor',
|
||||
'meta.color as meta_color'
|
||||
)
|
||||
->join('calendarinstances as ci', 'ci.calendarid', '=', 'calendars.id')
|
||||
->leftJoin('calendar_meta as meta', 'meta.calendar_id', '=', 'calendars.id')
|
||||
->where('ci.principaluri', $principal)
|
||||
->orderBy('ci.displayname')
|
||||
->with(['meta']) // no need to eager-load full instances any more
|
||||
->get();
|
||||
|
||||
return view('calendars.index', compact('calendars'));
|
||||
// get all the events in one query
|
||||
$events = Event::forCalendarsInRange(
|
||||
$calendars->pluck('id'),
|
||||
$range['start'],
|
||||
$range['end']
|
||||
);
|
||||
|
||||
// create the calendar grid of days
|
||||
$grid = $this->buildCalendarGrid($view, $range, $events);
|
||||
|
||||
// format the data for the frontend
|
||||
$payload = [
|
||||
'view' => $view,
|
||||
'range' => $range,
|
||||
'calendars' => $calendars->keyBy('id')->map(function ($cal) {
|
||||
return [
|
||||
'id' => $cal->id,
|
||||
'name' => $cal->displayname,
|
||||
'color' => $cal->meta_color ?? $cal->calendarcolor ?? '#999',
|
||||
'on' => true, // default to visible; the UI can toggle this
|
||||
];
|
||||
}),
|
||||
'events' => $events->map(function ($e) { // just the events map
|
||||
// fall back to Sabre timestamps if meta is missing
|
||||
$start = $e->meta->start_at
|
||||
?? Carbon::createFromTimestamp($e->firstoccurence);
|
||||
$end = $e->meta->end_at
|
||||
?? ($e->lastoccurence ? Carbon::createFromTimestamp($e->lastoccurence) : null);
|
||||
|
||||
return [
|
||||
'id' => $e->id,
|
||||
'calendar_id' => $e->calendarid,
|
||||
'title' => $e->meta->title ?? '(no title)',
|
||||
'start' => $start->format('c'),
|
||||
'end' => optional($end)->format('c'),
|
||||
];
|
||||
}),
|
||||
'grid' => $grid,
|
||||
];
|
||||
|
||||
return view('calendar.index', $payload);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('calendars.create');
|
||||
return view('calendar.create');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -71,7 +134,7 @@ class CalendarController extends Controller
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return redirect()->route('calendars.index');
|
||||
return redirect()->route('calendar.index');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -97,7 +160,7 @@ class CalendarController extends Controller
|
||||
->get();
|
||||
|
||||
return view(
|
||||
'calendars.show',
|
||||
'calendar.show',
|
||||
compact('calendar', 'instance', 'events', 'caldavUrl')
|
||||
);
|
||||
}
|
||||
@ -117,7 +180,7 @@ class CalendarController extends Controller
|
||||
|
||||
$instance = $calendar->instances->first(); // may be null but shouldn’t
|
||||
|
||||
return view('calendars.edit', compact('calendar', 'instance'));
|
||||
return view('calendar.edit', compact('calendar', 'instance'));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -153,7 +216,7 @@ class CalendarController extends Controller
|
||||
);
|
||||
|
||||
return redirect()
|
||||
->route('calendars.show', $calendar)
|
||||
->route('calendar.show', $calendar)
|
||||
->with('toast', __('Calendar saved successfully!'));
|
||||
}
|
||||
|
||||
@ -164,6 +227,134 @@ class CalendarController extends Controller
|
||||
{
|
||||
$this->authorize('delete', $calendar);
|
||||
$calendar->delete(); // cascades to meta via FK
|
||||
return redirect()->route('calendars.index');
|
||||
return redirect()->route('calendar.index');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Private helpers
|
||||
*/
|
||||
|
||||
/**
|
||||
* normalise $view and $date into a carbon range
|
||||
*
|
||||
* @return array [$view, ['start' => Carbon, 'end' => Carbon]]
|
||||
*/
|
||||
private function resolveRange(Request $request): array
|
||||
{
|
||||
// get the view
|
||||
$view = in_array($request->query('view'), ['week', '4day'])
|
||||
? $request->query('view')
|
||||
: 'month';
|
||||
|
||||
// anchor date in the user's timezone
|
||||
$anchor = Carbon::parse($request->query('date', now()->toDateString()))
|
||||
->setTimezone(auth()->user()->timezone ?? config('app.timezone'));
|
||||
|
||||
// set dates based on view
|
||||
switch ($view) {
|
||||
case 'week':
|
||||
$start = $anchor->copy()->startOfWeek();
|
||||
$end = $anchor->copy()->endOfWeek();
|
||||
break;
|
||||
|
||||
case '4day':
|
||||
// a rolling 4-day "agenda" view starting at anchor
|
||||
$start = $anchor->copy()->startOfDay();
|
||||
$end = $anchor->copy()->addDays(3)->endOfDay();
|
||||
break;
|
||||
|
||||
default: // month
|
||||
$start = $anchor->copy()->startOfMonth();
|
||||
$end = $anchor->copy()->endOfMonth();
|
||||
}
|
||||
|
||||
return [$view, ['start' => $start, 'end' => $end]];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Assemble an array of day-objects for the requested view.
|
||||
*
|
||||
* Day object shape:
|
||||
* [
|
||||
* 'date' => '2025-07-14',
|
||||
* 'label' => '14', // two-digit day number
|
||||
* 'in_month' => true|false, // helpful for grey-out styling
|
||||
* 'events' => [ …event payloads… ]
|
||||
* ]
|
||||
*
|
||||
* For the "month" view the return value also contains
|
||||
* 'weeks' => [ [7 day-objs], [7 day-objs], … ]
|
||||
*/
|
||||
private function buildCalendarGrid(string $view, array $range, Collection $events): array
|
||||
{
|
||||
// index events by YYYY-MM-DD for quick lookup */
|
||||
$eventsByDay = [];
|
||||
foreach ($events as $ev) {
|
||||
$start = $ev->meta->start_at
|
||||
?? Carbon::createFromTimestamp($ev->firstoccurence);
|
||||
$end = $ev->meta->end_at
|
||||
?? ($ev->lastoccurence
|
||||
? Carbon::createFromTimestamp($ev->lastoccurence)
|
||||
: $start);
|
||||
|
||||
// spread multi-day events across each day they touch
|
||||
for ($d = $start->copy()->startOfDay();
|
||||
$d->lte($end->copy()->endOfDay());
|
||||
$d->addDay()) {
|
||||
|
||||
$key = $d->toDateString(); // e.g. '2025-07-14'
|
||||
$eventsByDay[$key] ??= [];
|
||||
$eventsByDay[$key][] = [
|
||||
'id' => $ev->id,
|
||||
'calendar_id' => $ev->calendarid,
|
||||
'title' => $ev->meta->title ?? '(no title)',
|
||||
'start' => $start->format('c'),
|
||||
'end' => $end->format('c'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// determine which individual days belong to this view */
|
||||
switch ($view) {
|
||||
case 'week':
|
||||
$gridStart = $range['start']->copy();
|
||||
$gridEnd = $range['start']->copy()->addDays(6);
|
||||
break;
|
||||
|
||||
case '4day':
|
||||
$gridStart = $range['start']->copy();
|
||||
$gridEnd = $range['start']->copy()->addDays(3);
|
||||
break;
|
||||
|
||||
default: // month
|
||||
$gridStart = $range['start']->copy()->startOfWeek(); // Sunday-start; tweak if needed
|
||||
$gridEnd = $range['end']->copy()->endOfWeek();
|
||||
}
|
||||
|
||||
// walk the span, build the day objects */
|
||||
$days = [];
|
||||
for ($day = $gridStart->copy(); $day->lte($gridEnd); $day->addDay()) {
|
||||
$iso = $day->toDateString();
|
||||
$isToday = $day->isSameDay(Carbon::today());
|
||||
$days[] = [
|
||||
'date' => $iso,
|
||||
'label' => $day->format('j'),
|
||||
'in_month' => $day->month === $range['start']->month,
|
||||
'is_today' => $isToday,
|
||||
'events' => $eventsByDay[$iso] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
// for a month view, also group into weeks
|
||||
if ($view === 'month') {
|
||||
$weeks = array_chunk($days, 7); // 7 days per week row
|
||||
return ['days' => $days, 'weeks' => $weeks];
|
||||
}
|
||||
|
||||
return ['days' => $days];
|
||||
}
|
||||
}
|
||||
|
@ -41,4 +41,30 @@ class Event extends Model
|
||||
{
|
||||
return $this->hasOne(EventMeta::class, 'event_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* filter events in event_meta by start and end time
|
||||
*/
|
||||
public function scopeInRange($query, $start, $end)
|
||||
{
|
||||
return $query->whereHas('meta', function ($q) use ($start, $end) {
|
||||
$q->where('start_at', '<=', $end)
|
||||
->where(function ($qq) use ($start) {
|
||||
$qq->where('end_at', '>=', $start)
|
||||
->orWhereNull('end_at'); // open-ended events
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ccnvenience wrapper for calendar controller
|
||||
*/
|
||||
public static function forCalendarsInRange($calendarIds, $start, $end)
|
||||
{
|
||||
return static::query()
|
||||
->with('meta') // eager-load meta once
|
||||
->whereIn('calendarid', $calendarIds)
|
||||
->inRange($start, $end) // ← the scope above
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,9 @@
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"blade-ui-kit/blade-icons": "^1.8",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"omnia-digital/livewire-calendar": "^3.2",
|
||||
"sabre/dav": "^4.7"
|
||||
},
|
||||
"require-dev": {
|
||||
|
232
composer.lock
generated
@ -4,8 +4,89 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "7376fcf2b57a1242a21836781b34e006",
|
||||
"content-hash": "d70fc92c0f938b08d8e0050b99c7ed1c",
|
||||
"packages": [
|
||||
{
|
||||
"name": "blade-ui-kit/blade-icons",
|
||||
"version": "1.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/driesvints/blade-icons.git",
|
||||
"reference": "7b743f27476acb2ed04cb518213d78abe096e814"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/driesvints/blade-icons/zipball/7b743f27476acb2ed04cb518213d78abe096e814",
|
||||
"reference": "7b743f27476acb2ed04cb518213d78abe096e814",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/filesystem": "^8.0|^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/view": "^8.0|^9.0|^10.0|^11.0|^12.0",
|
||||
"php": "^7.4|^8.0",
|
||||
"symfony/console": "^5.3|^6.0|^7.0",
|
||||
"symfony/finder": "^5.3|^6.0|^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.5.1",
|
||||
"orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0",
|
||||
"phpunit/phpunit": "^9.0|^10.5|^11.0"
|
||||
},
|
||||
"bin": [
|
||||
"bin/blade-icons-generate"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"BladeUI\\Icons\\BladeIconsServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"BladeUI\\Icons\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Dries Vints",
|
||||
"homepage": "https://driesvints.com"
|
||||
}
|
||||
],
|
||||
"description": "A package to easily make use of icons in your Laravel Blade views.",
|
||||
"homepage": "https://github.com/blade-ui-kit/blade-icons",
|
||||
"keywords": [
|
||||
"blade",
|
||||
"icons",
|
||||
"laravel",
|
||||
"svg"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/blade-ui-kit/blade-icons/issues",
|
||||
"source": "https://github.com/blade-ui-kit/blade-icons"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sponsors/driesvints",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://www.paypal.com/paypalme/driesvints",
|
||||
"type": "paypal"
|
||||
}
|
||||
],
|
||||
"time": "2025-02-13T20:35:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
"version": "0.13.1",
|
||||
@ -2006,82 +2087,6 @@
|
||||
],
|
||||
"time": "2024-12-08T08:18:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "livewire/livewire",
|
||||
"version": "v3.6.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/livewire/livewire.git",
|
||||
"reference": "ef04be759da41b14d2d129e670533180a44987dc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/livewire/livewire/zipball/ef04be759da41b14d2d129e670533180a44987dc",
|
||||
"reference": "ef04be759da41b14d2d129e670533180a44987dc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/database": "^10.0|^11.0|^12.0",
|
||||
"illuminate/routing": "^10.0|^11.0|^12.0",
|
||||
"illuminate/support": "^10.0|^11.0|^12.0",
|
||||
"illuminate/validation": "^10.0|^11.0|^12.0",
|
||||
"laravel/prompts": "^0.1.24|^0.2|^0.3",
|
||||
"league/mime-type-detection": "^1.9",
|
||||
"php": "^8.1",
|
||||
"symfony/console": "^6.0|^7.0",
|
||||
"symfony/http-kernel": "^6.2|^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"calebporzio/sushi": "^2.1",
|
||||
"laravel/framework": "^10.15.0|^11.0|^12.0",
|
||||
"mockery/mockery": "^1.3.1",
|
||||
"orchestra/testbench": "^8.21.0|^9.0|^10.0",
|
||||
"orchestra/testbench-dusk": "^8.24|^9.1|^10.0",
|
||||
"phpunit/phpunit": "^10.4|^11.5",
|
||||
"psy/psysh": "^0.11.22|^0.12"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"aliases": {
|
||||
"Livewire": "Livewire\\Livewire"
|
||||
},
|
||||
"providers": [
|
||||
"Livewire\\LivewireServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Livewire\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Caleb Porzio",
|
||||
"email": "calebporzio@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "A front-end framework for Laravel.",
|
||||
"support": {
|
||||
"issues": "https://github.com/livewire/livewire/issues",
|
||||
"source": "https://github.com/livewire/livewire/tree/v3.6.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/livewire",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-07-17T05:12:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
"version": "3.9.0",
|
||||
@ -2583,79 +2588,6 @@
|
||||
],
|
||||
"time": "2025-05-08T08:14:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "omnia-digital/livewire-calendar",
|
||||
"version": "3.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/omnia-digital/livewire-calendar.git",
|
||||
"reference": "9488ebaa84bf96f09c25dfbc2538d394aec365a7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/omnia-digital/livewire-calendar/zipball/9488ebaa84bf96f09c25dfbc2538d394aec365a7",
|
||||
"reference": "9488ebaa84bf96f09c25dfbc2538d394aec365a7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
|
||||
"livewire/livewire": "^2.0||^3.0",
|
||||
"php": "^7.2|^8.0|^8.1|^8.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"orchestra/testbench": "^5.0|^6.0",
|
||||
"phpunit/phpunit": "^8.0|^9.0|^10.0|^11.0|^12.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"aliases": {
|
||||
"LivewireCalendar": "Omnia\\LivewireCalendar\\LivewireCalendarFacade"
|
||||
},
|
||||
"providers": [
|
||||
"Omnia\\LivewireCalendar\\LivewireCalendarServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Omnia\\LivewireCalendar\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Josh Torres",
|
||||
"email": "josht@omniadigital.io",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Andrés Santibáñez",
|
||||
"email": "santibanez.andres@gmail.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Osei Quashie",
|
||||
"email": "osei@omniadigital.io",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Laravel Livewire calendar component",
|
||||
"homepage": "https://github.com/omnia-digital/livewire-calendar",
|
||||
"keywords": [
|
||||
"livewire-calendar",
|
||||
"omnia",
|
||||
"omnia-digital"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/omnia-digital/livewire-calendar/issues",
|
||||
"source": "https://github.com/omnia-digital/livewire-calendar/tree/3.2.0"
|
||||
},
|
||||
"time": "2025-02-28T18:18:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
"version": "1.9.3",
|
||||
|
114
config/blade-icons.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Icons Sets
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| With this config option you can define a couple of
|
||||
| default icon sets. Provide a key name for your icon
|
||||
| set and a combination from the options below.
|
||||
|
|
||||
*/
|
||||
|
||||
'sets' => [
|
||||
|
||||
'default' => [
|
||||
/* relative path from app root for svg icons */
|
||||
'path' => 'resources/svg/icons',
|
||||
/* specific filesystem disk from which to read icons */
|
||||
'disk' => '',
|
||||
/* default prefix for icon elements */
|
||||
'prefix' => 'icon',
|
||||
/* fallback when an icon in this set cannot be found */
|
||||
'fallback' => 'home',
|
||||
/* default classes applied to icons in this set */
|
||||
'class' => '',
|
||||
/* default set attributes for these icons */
|
||||
'attributes' => [],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global Default Classes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This config option allows you to define some classes which
|
||||
| will be applied by default to all icons.
|
||||
|
|
||||
*/
|
||||
|
||||
'class' => '',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global Default Attributes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This config option allows you to define some attributes which
|
||||
| will be applied by default to all icons.
|
||||
|
|
||||
*/
|
||||
|
||||
'attributes' => [
|
||||
'width' => 24,
|
||||
'height' => 24,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global Fallback Icon
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This config option allows you to define a global fallback
|
||||
| icon when an icon in any set cannot be found. It can
|
||||
| reference any icon from any configured set.
|
||||
|
|
||||
*/
|
||||
|
||||
'fallback' => '',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Components
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These config options allow you to define some
|
||||
| settings related to Blade Components.
|
||||
|
|
||||
*/
|
||||
|
||||
'components' => [
|
||||
|
||||
/*
|
||||
|----------------------------------------------------------------------
|
||||
| Disable Components
|
||||
|----------------------------------------------------------------------
|
||||
|
|
||||
| This config option allows you to disable Blade components
|
||||
| completely. It's useful to avoid performance problems
|
||||
| when working with large icon libraries.
|
||||
|
|
||||
*/
|
||||
|
||||
'disabled' => false,
|
||||
|
||||
/*
|
||||
|----------------------------------------------------------------------
|
||||
| Default Icon Component Name
|
||||
|----------------------------------------------------------------------
|
||||
|
|
||||
| This config option allows you to define the name
|
||||
| for the default Icon class component.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => 'icon',
|
||||
|
||||
],
|
||||
|
||||
];
|
@ -11,10 +11,13 @@ return new class extends Migration
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary(); // ulid primary key
|
||||
$table->string('uri')->nullable(); // formerly from sabre principals table
|
||||
$table->string('firstname')->nullable();
|
||||
$table->string('lastname')->nullable();
|
||||
$table->string('displayname')->nullable(); // formerly from sabre principals table
|
||||
$table->string('name')->nullable(); // custom name if necessary
|
||||
//$table->string('name')->nullable(); // custom name if necessary
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('timezone', 64)->default('UTC');
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
|
@ -14,15 +14,19 @@ class DatabaseSeeder extends Seeder
|
||||
public function run(): void
|
||||
{
|
||||
/** credentials from .env (with sensible fall-backs) */
|
||||
$email = env('ADMIN_EMAIL', 'admin@example.com');
|
||||
$password = env('ADMIN_PASSWORD', 'changeme');
|
||||
$name = env('ADMIN_NAME', 'Admin');
|
||||
$email = env('ADMIN_EMAIL', 'admin@example.com');
|
||||
$password = env('ADMIN_PASSWORD', 'changeme');
|
||||
$firstname = env('ADMIN_FIRSTNAME', 'Admin');
|
||||
$lastname = env('ADMIN_LASTNAME', 'Account');
|
||||
$timezone = env('APP_TIMEZONE', 'UTC');
|
||||
|
||||
/** create or update the admin user */
|
||||
$user = User::updateOrCreate(
|
||||
['email' => $email],
|
||||
[
|
||||
'name' => $name,
|
||||
'firstname' => $firstname,
|
||||
'lastname' => $lastname,
|
||||
'timezone' => $timezone,
|
||||
'password' => Hash::make($password),
|
||||
]
|
||||
);
|
||||
@ -30,7 +34,7 @@ class DatabaseSeeder extends Seeder
|
||||
/** fill the sabre-friendly columns */
|
||||
$user->update([
|
||||
'uri' => 'principals/'.$user->email,
|
||||
'displayname' => $user->name,
|
||||
'displayname' => $firstname.' '.$lastname,
|
||||
]);
|
||||
|
||||
/** sample caldav data */
|
||||
|
1428
package-lock.json
generated
@ -8,13 +8,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"axios": "^1.8.2",
|
||||
"concurrently": "^9.0.1",
|
||||
"laravel-vite-plugin": "^1.2.0",
|
||||
"postcss": "^8.4.31",
|
||||
"tailwindcss": "^3.1.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"vite": "^6.2.4"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
@ -1,3 +1,27 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
/** tailwind */
|
||||
@import 'tailwindcss';
|
||||
@import './etc/theme.css';
|
||||
|
||||
/** kithkin */
|
||||
@import './etc/layout.css';
|
||||
@import './etc/type.css';
|
||||
@import './lib/button.css';
|
||||
@import './lib/mini.css';
|
||||
|
||||
/** plugins */
|
||||
@plugin '@tailwindcss/forms';
|
||||
|
||||
/** laravel package views */
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/**/*.blade.php';
|
||||
@source '../../storage/framework/views/**/*.php';
|
||||
|
||||
/** tailwind v4 corrections */
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentcolor);
|
||||
}
|
||||
}
|
||||
|
140
resources/css/etc/layout.css
Normal file
@ -0,0 +1,140 @@
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply m-0 p-0 w-dvw h-dvh min-w-dvw min-h-dvh bg-gray-100 font-sans antialiased;
|
||||
|
||||
&#app {
|
||||
@apply grid;
|
||||
grid-template-columns: 5rem auto;
|
||||
grid-template-rows: 1fr 0;
|
||||
}
|
||||
|
||||
&#auth {
|
||||
@apply flex items-center justify-center;
|
||||
|
||||
header {
|
||||
@apply flex flex-row items-center justify-between px-6 fixed top-0 left-0 h-20;
|
||||
|
||||
a {
|
||||
@apply inline-flex items-center gap-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* primary app navigation on the left */
|
||||
nav {
|
||||
@apply w-20 flex flex-col items-center justify-between;
|
||||
|
||||
/* top items */
|
||||
.top {
|
||||
@apply flex flex-col items-center pt-6 2xl:pt-8 mt-2px;
|
||||
}
|
||||
|
||||
/* bottom items */
|
||||
|
||||
.bottom {
|
||||
@apply pb-6 2xl:pb-8;
|
||||
}
|
||||
|
||||
/* app buttons */
|
||||
menu {
|
||||
@apply flex flex-col gap-1 items-center mt-6;
|
||||
|
||||
li.app-button {
|
||||
|
||||
a {
|
||||
@apply flex items-center justify-center p-3 bg-transparent text-black;
|
||||
transition: background-color 100ms ease-in-out;
|
||||
border-radius: 70% 50% 70% 30% / 60% 60% 60% 40%; /* blob 1 */
|
||||
|
||||
&:hover {
|
||||
@apply bg-gray-200;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
@apply bg-cyan-400;
|
||||
|
||||
&:hover {
|
||||
@apply bg-cyan-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) a {
|
||||
border-radius: 70% 30% 30% 70% / 60% 40% 60% 40%; /* blob 2 */
|
||||
}
|
||||
|
||||
&:nth-child(3) a {
|
||||
border-radius: 80% 65% 90% 50% / 90% 80% 75% 75%; /* blob 3 */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* primary content window defaults */
|
||||
main {
|
||||
@apply rounded-lg bg-white;
|
||||
|
||||
body#app & {
|
||||
@apply grid m-2 ml-0;
|
||||
grid-template-rows: 5rem auto;
|
||||
}
|
||||
|
||||
body#auth & {
|
||||
@apply w-1/2 mx-auto p-8;
|
||||
min-width: 16rem;
|
||||
max-width: 40rem;
|
||||
}
|
||||
|
||||
/* main content title and actions */
|
||||
> header {
|
||||
@apply flex flex-row items-center justify-between px-6 2xl:px-8;
|
||||
|
||||
h1 {
|
||||
@apply h-12 max-h-12;
|
||||
}
|
||||
|
||||
menu {
|
||||
@apply flex flex-row items-center justify-end gap-2 h-12 max-h-12;
|
||||
}
|
||||
}
|
||||
|
||||
/* main content wrapper */
|
||||
> article {
|
||||
@apply grid w-full;
|
||||
grid-template-columns: minmax(20rem, 20dvw) repeat(3, 1fr);
|
||||
|
||||
/* left column */
|
||||
aside {
|
||||
@apply col-span-1 px-6 2xl:px-8 h-full;
|
||||
}
|
||||
|
||||
/* calendar page defaults */
|
||||
&#calendar {
|
||||
|
||||
aside {
|
||||
@apply grid pb-6 2xl:pb-8;
|
||||
grid-template-rows: 1fr min-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (width >= 96rem) { /* 2xl */
|
||||
main {
|
||||
body#app & {
|
||||
grid-template-rows: 6rem auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* app logo */
|
||||
.logo {
|
||||
@apply w-10 h-10 flex;
|
||||
|
||||
.overlay {
|
||||
fill: var(--color-cyan-500);
|
||||
}
|
||||
}
|
47
resources/css/etc/theme.css
Normal file
@ -0,0 +1,47 @@
|
||||
@theme {
|
||||
--font-sans: ui-sans-serif, system-ui, Inter, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: Chewie, ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
|
||||
--color-gray-50: #f6f6f6;
|
||||
--color-gray-100: #eeeeee;
|
||||
--color-gray-200: #dddddd;
|
||||
--color-gray-300: #cfcfcf;
|
||||
--color-gray-400: #bababa;
|
||||
--color-gray-500: #a0a0a0;
|
||||
--color-gray-600: #999999;
|
||||
--color-gray-700: #777777;
|
||||
--color-gray-800: #555555;
|
||||
--color-gray-900: #4a4a4a;
|
||||
--color-gray-950: #282828;
|
||||
--color-primary: #151515;
|
||||
--color-primary-hover: #000000;
|
||||
--color-cyan-50: oklch(98.97% 0.015 196.79);
|
||||
--color-cyan-100: oklch(97.92% 0.03 196.61);
|
||||
--color-cyan-200: oklch(95.79% 0.063 196.12);
|
||||
--color-cyan-300: oklch(94.76% 0.079 195.87);
|
||||
--color-cyan-400: oklch(92.6% 0.117 195.31);
|
||||
--color-cyan-500: oklch(90.54% 0.155 194.76); /* 00ffff */
|
||||
--color-cyan-550: oklch(82% 0.2812 194.769); /* 00e3e3 */
|
||||
|
||||
--border-width-1.5: 1.5px;
|
||||
|
||||
--radius-xs: 0.25rem;
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.6667rem;
|
||||
--radius-lg: 1rem;
|
||||
--radius-xl: 1.25rem;
|
||||
--radius-2xl: 1.5rem;
|
||||
--radius-3xl: 2rem;
|
||||
--radius-4xl: 3rem;
|
||||
--radius-blob: 80% 65% 90% 50% / 90% 80% 75% 75%;
|
||||
|
||||
--shadow-drop: 2.5px 2.5px 0 0 var(--color-primary);
|
||||
|
||||
--spacing-2px: 2px;
|
||||
|
||||
--text-3xl: 2rem;
|
||||
--text-3xl--line-height: calc(2.25 / 1.875);
|
||||
--text-4xl: 3rem;
|
||||
--text-4xl--line-height: 1;
|
||||
}
|
29
resources/css/etc/type.css
Normal file
@ -0,0 +1,29 @@
|
||||
@font-face {
|
||||
font-family: 'Fraunces';
|
||||
src: url('../font/fraunces-variable.ttf') format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Recoleta';
|
||||
src: url('../font/recoleta-bold.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Chewie';
|
||||
src: url('../font/chewie-bold.otf') format('opentype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Analogue';
|
||||
src: url('../font/analogue-bold.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply font-serif text-3xl font-extrabold leading-tight;
|
||||
}
|
33
resources/css/lib/button.css
Normal file
@ -0,0 +1,33 @@
|
||||
button,
|
||||
.button {
|
||||
@apply relative inline-flex items-center cursor-pointer gap-2 rounded-md h-11 px-4 text-lg font-medium;
|
||||
transition: background-color 100ms ease-in-out;
|
||||
--button-border: var(--color-primary);
|
||||
--button-accent: var(--color-primary-hover);
|
||||
|
||||
&.button--primary {
|
||||
@apply bg-cyan-300;
|
||||
border: 1.5px solid var(--button-border);
|
||||
box-shadow: 2.5px 2.5px 0 0 var(--button-border);
|
||||
|
||||
&:hover {
|
||||
@apply bg-cyan-400;
|
||||
border-color: var(--button-accent);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
left: 2.5px;
|
||||
top: 2.5px;
|
||||
}
|
||||
}
|
||||
|
||||
&.button--icon {
|
||||
@apply justify-center p-0 h-12 top-px rounded-blob;
|
||||
aspect-ratio: 1 / 1;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0,0,0,0.075);
|
||||
}
|
||||
}
|
||||
}
|
74
resources/css/lib/mini.css
Normal file
@ -0,0 +1,74 @@
|
||||
.mini {
|
||||
@apply w-full;
|
||||
|
||||
/* mini controls */
|
||||
header{
|
||||
@apply flex items-center justify-between px-2 pb-3;
|
||||
|
||||
> span {
|
||||
@apply font-serif text-lg;
|
||||
}
|
||||
}
|
||||
|
||||
/* days wrapper */
|
||||
figure {
|
||||
@apply border-1.5 border-primary shadow-drop rounded-md;
|
||||
|
||||
/* weekdays */
|
||||
figcaption {
|
||||
@apply grid grid-cols-7 p-2 pt-3 pb-0;
|
||||
|
||||
span {
|
||||
@apply flex items-center justify-center font-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
/* day grid wrapper */
|
||||
form {
|
||||
@apply grid grid-cols-7 p-2 pt-1;
|
||||
}
|
||||
|
||||
/* day */
|
||||
.day {
|
||||
@apply text-base p-0 relative flex items-center justify-center h-auto rounded-blob;
|
||||
aspect-ratio: 1 / 1;
|
||||
|
||||
&:hover {
|
||||
@apply bg-gray-50;
|
||||
}
|
||||
|
||||
&.day--current {
|
||||
|
||||
}
|
||||
|
||||
&.day--outside {
|
||||
@apply text-gray-500;
|
||||
}
|
||||
|
||||
&.day--today {
|
||||
@apply bg-cyan-500 font-bold;
|
||||
|
||||
&:hover {
|
||||
@apply bg-cyan-550;
|
||||
}
|
||||
}
|
||||
|
||||
&.day--with-events {
|
||||
&::after {
|
||||
@apply absolute bottom-0 left-1/2 -translate-x-1/2 h-1 rounded-full w-4 bg-yellow-500;
|
||||
content: '';
|
||||
}
|
||||
&[data-event-count='1']::after {
|
||||
@apply w-1;
|
||||
}
|
||||
&[data-event-count='2']::after {
|
||||
@apply w-2;
|
||||
}
|
||||
&[data-event-count='3']::after {
|
||||
@apply w-3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
BIN
resources/font/analogue-bold.woff2
Normal file
BIN
resources/font/chewie-bold.otf
Normal file
BIN
resources/font/fraunces-italic.ttf
Normal file
BIN
resources/font/fraunces-variable.ttf
Normal file
BIN
resources/font/recoleta-bold.woff2
Normal file
1
resources/svg/icons/book-user.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-user-icon lucide-book-user"><path d="M15 13a3 3 0 1 0-6 0"/><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/><circle cx="12" cy="8" r="2"/></svg>
|
After Width: | Height: | Size: 401 B |
1
resources/svg/icons/calendar.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar-icon lucide-calendar"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/></svg>
|
After Width: | Height: | Size: 345 B |
1
resources/svg/icons/home.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-house-icon lucide-house"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
|
After Width: | Height: | Size: 408 B |
1
resources/svg/icons/notebook.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-notebook-icon lucide-notebook"><path d="M2 6h4"/><path d="M2 10h4"/><path d="M2 14h4"/><path d="M2 18h4"/><rect width="16" height="20" x="4" y="2" rx="2"/><path d="M16 2v20"/></svg>
|
After Width: | Height: | Size: 383 B |
1
resources/svg/icons/plus-circle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-plus-icon lucide-circle-plus"><circle cx="12" cy="12" r="10"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>
|
After Width: | Height: | Size: 315 B |
1
resources/svg/icons/plus.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus-icon lucide-plus"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
After Width: | Height: | Size: 271 B |
1
resources/svg/icons/settings.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-settings-icon lucide-settings"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
|
After Width: | Height: | Size: 847 B |
1
resources/svg/icons/user-circle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-user-round-icon lucide-circle-user-round"><path d="M18 20a6 6 0 0 0-12 0"/><circle cx="12" cy="10" r="4"/><circle cx="12" cy="12" r="10"/></svg>
|
After Width: | Height: | Size: 353 B |
@ -27,21 +27,21 @@
|
||||
<!-- 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">
|
||||
<input id="remember_me" type="checkbox" class="rounded-sm border-gray-300 text-indigo-600 shadow-xs focus:ring-indigo-500" name="remember">
|
||||
<span class="ms-2 text-sm text-gray-600">{{ __('Remember me') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<div class="flex items-center justify-between mt-4 gap-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="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('password.request') }}">
|
||||
{{ __('Forgot your password?') }}
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<x-primary-button class="ms-3">
|
||||
<x-button variant="primary" type="submit">
|
||||
{{ __('Log in') }}
|
||||
</x-primary-button>
|
||||
</x-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-guest-layout>
|
||||
|
@ -40,7 +40,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="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('login') }}">
|
||||
{{ __('Already registered?') }}
|
||||
</a>
|
||||
|
||||
|
@ -23,7 +23,7 @@
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
|
||||
<button type="submit" 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">
|
||||
<button type="submit" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
{{ __('Log Out') }}
|
||||
</button>
|
||||
</form>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||
|
||||
{{-- Books list --}}
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="bg-white shadow-sm sm:rounded-lg">
|
||||
<ul class="divide-y divide-gray-200">
|
||||
@forelse($books as $book)
|
||||
<li class="px-6 py-4 flex items-center justify-between">
|
||||
|
@ -18,7 +18,7 @@
|
||||
<div>
|
||||
<x-input-label for="description" :value="__('Description')" />
|
||||
<textarea id="description" name="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md shadow-sm border-gray-300 focus:border-indigo-300 focus:ring">{{ old('description', $instance?->description ?? '') }}</textarea>
|
||||
class="mt-1 block w-full rounded-md shadow-xs border-gray-300 focus:border-indigo-300 focus:ring-3">{{ old('description', $instance?->description ?? '') }}</textarea>
|
||||
<x-input-error class="mt-2" :messages="$errors->get('description')" />
|
||||
</div>
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
<div>
|
||||
<x-input-label for="timezone" :value="__('Timezone')" />
|
||||
<select id="timezone" name="timezone"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 focus:border-indigo-300 focus:ring">
|
||||
class="mt-1 block w-full rounded-md border-gray-300 focus:border-indigo-300 focus:ring-3">
|
||||
@foreach(timezone_identifiers_list() as $tz)
|
||||
<option value="{{ $tz }}"
|
||||
@selected(old('timezone', $instance?->timezone ?? config('app.timezone')) === $tz)>
|
@ -7,8 +7,8 @@
|
||||
|
||||
<div class="py-6">
|
||||
<div class="max-w-2xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white shadow sm:rounded-lg p-6">
|
||||
<form method="POST" action="{{ route('calendars.store') }}">
|
||||
<div class="bg-white shadow-sm sm:rounded-lg p-6">
|
||||
<form method="POST" action="{{ route('calendar.store') }}">
|
||||
@csrf
|
||||
|
||||
{{-- just render the form component --}}
|
@ -7,8 +7,8 @@
|
||||
|
||||
<div class="py-6">
|
||||
<div class="max-w-2xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white shadow sm:rounded-lg p-6">
|
||||
<form method="POST" action="{{ route('calendars.update', $calendar) }}">
|
||||
<div class="bg-white shadow-sm sm:rounded-lg p-6">
|
||||
<form method="POST" action="{{ route('calendar.update', $calendar) }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
43
resources/views/calendar/index.blade.php
Normal file
@ -0,0 +1,43 @@
|
||||
<x-app-layout id="calendar">
|
||||
<x-slot name="header">
|
||||
<h1>
|
||||
{{ __('Calendar') }}
|
||||
</h1>
|
||||
<menu>
|
||||
<li>
|
||||
<a class="button button--primary" href="{{ route('calendar.create') }}">
|
||||
<x-icon-plus-circle /> Create
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="button button--icon" href="{{ route('calendar.create') }}">
|
||||
<x-icon-settings />
|
||||
</a>
|
||||
</li>
|
||||
</menu>
|
||||
</x-slot>
|
||||
<x-slot name="article">
|
||||
<aside>
|
||||
<div>
|
||||
@foreach ($calendars as $cal)
|
||||
<label class="flex items-center space-x-2">
|
||||
<input type="checkbox"
|
||||
wire:model="visibleCalendars"
|
||||
value="{{ $cal['id'] }}"
|
||||
checked>
|
||||
<span class="w-3 h-3 rounded-sm" style="background: {{ $cal['color'] }}"></span>
|
||||
<span>{{ $cal['name'] }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<x-calendar.mini>
|
||||
@foreach ($grid['weeks'] as $week)
|
||||
@foreach ($week as $day)
|
||||
<x-calendar.mini-day :day="$day" />
|
||||
@endforeach
|
||||
@endforeach
|
||||
</x-calendar.mini>
|
||||
</aside>
|
||||
</x-slot>
|
||||
</x-app-layout>
|
@ -23,7 +23,7 @@
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||
|
||||
{{-- Calendar meta --}}
|
||||
<div class="bg-white shadow sm:rounded-lg p-6">
|
||||
<div class="bg-white shadow-sm sm:rounded-lg p-6">
|
||||
<dl class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">{{ __('Description') }}</dt>
|
||||
@ -67,7 +67,7 @@
|
||||
</div>
|
||||
|
||||
{{-- Events list --}}
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="bg-white shadow-sm sm:rounded-lg">
|
||||
<div class="px-6 py-4 border-b font-semibold">
|
||||
{{ __('Events') }}
|
||||
</div>
|
@ -1,40 +0,0 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="text-xl font-semibold leading-tight">
|
||||
{{ __('My Calendars') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-6">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||
|
||||
{{-- “New Calendar” button --}}
|
||||
<div class="flex justify-end">
|
||||
<a href="{{ route('calendars.create') }}"
|
||||
class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded-md shadow">
|
||||
+ {{ __('New Calendar') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- Calendars list --}}
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<ul class="divide-y divide-gray-200">
|
||||
@forelse($calendars as $calendar)
|
||||
<li class="px-6 py-4 flex items-center justify-between">
|
||||
<a href="{{ route('calendars.show', $calendar) }}" class="font-medium text-indigo-600">
|
||||
{{ $calendar->instance_displayname }}
|
||||
</a>
|
||||
|
||||
<span class="text-sm text-gray-500">
|
||||
{{ $calendar->events()->count() }} {{ __('events') }}
|
||||
</span>
|
||||
</li>
|
||||
@empty
|
||||
<li class="px-6 py-4">{{ __('No calendars yet.') }}</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
@ -1,3 +1,4 @@
|
||||
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
|
||||
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" {{ $attributes }}>
|
||||
<path d="M215.8,119.6l-69.26,70.06a8,8,0,0,1-5.65,2.34H64.2V115.31a8,8,0,0,1,2.34-5.65L112.2,64.52V144l24-24Z" opacity="0.2" class="overlay"></path>
|
||||
<path d="M221.28,34.75a64,64,0,0,0-90.49,0L60.69,104A15.9,15.9,0,0,0,56,115.31v73.38L26.34,218.34a8,8,0,0,0,11.32,11.32L67.32,200H140.7A15.92,15.92,0,0,0,152,195.32l0,0,69.23-70A64,64,0,0,0,221.28,34.75ZM142.07,46.06A48,48,0,0,1,211.79,112H155.33l34.35-34.34a8,8,0,0,0-11.32-11.32L120,124.69V67.87ZM72,115.35l32-31.67v57l-32,32ZM140.7,184H83.32l56-56h56.74Z"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 632 B |
24
resources/views/components/button/icon.blade.php
Normal file
@ -0,0 +1,24 @@
|
||||
@props([
|
||||
'variant' => '',
|
||||
'size' => 'default',
|
||||
'type' => 'button',
|
||||
'class' => '',
|
||||
'label' => 'Icon button' ])
|
||||
|
||||
@php
|
||||
$variantClass = match ($variant) {
|
||||
'primary' => 'button--primary',
|
||||
'secondary' => 'button--secondary',
|
||||
default => '',
|
||||
};
|
||||
|
||||
$sizeClass = match ($size) {
|
||||
'sm' => 'button--sm',
|
||||
'lg' => 'button--lg',
|
||||
default => '',
|
||||
};
|
||||
@endphp
|
||||
|
||||
<button type="{{ $type }}" class="button button--icon {{ $variantClass }} {{ $sizeClass }} {{ $class }}" aria-label="{{ $label }}">
|
||||
{{ $slot }}
|
||||
</button>
|
24
resources/views/components/button/index.blade.php
Normal file
@ -0,0 +1,24 @@
|
||||
@props([
|
||||
'variant' => '',
|
||||
'size' => 'default',
|
||||
'type' => 'button',
|
||||
'class' => '',
|
||||
'label' => 'Icon button' ])
|
||||
|
||||
@php
|
||||
$variantClass = match ($variant) {
|
||||
'primary' => 'button--primary',
|
||||
'secondary' => 'button--secondary',
|
||||
default => '',
|
||||
};
|
||||
|
||||
$sizeClass = match ($size) {
|
||||
'sm' => 'button--sm',
|
||||
'lg' => 'button--lg',
|
||||
default => '',
|
||||
};
|
||||
@endphp
|
||||
|
||||
<button type="{{ $type }}" class="{{ $variantClass }} {{ $sizeClass }} {{ $class }}">
|
||||
{{ $slot }}
|
||||
</button>
|
12
resources/views/components/calendar/mini-day.blade.php
Normal file
@ -0,0 +1,12 @@
|
||||
<button
|
||||
type="submit"
|
||||
data-event-count="{{ count($day['events'] ?? []) }}"
|
||||
@class([
|
||||
'day' => true,
|
||||
'day--with-events' => !empty($day['events']),
|
||||
'day--current' => $day['in_month'],
|
||||
'day--outside' => !$day['in_month'],
|
||||
'day--today' => $day['is_today'],
|
||||
])>
|
||||
<span class="">{{ $day['label'] }}</span>
|
||||
</div>
|
22
resources/views/components/calendar/mini.blade.php
Normal file
@ -0,0 +1,22 @@
|
||||
@props(['class' => ''])
|
||||
|
||||
<section class="mini mini--month {{ $class }}">
|
||||
<header>
|
||||
<span>July 2025</span>
|
||||
<menu>Controls</menu>
|
||||
</header>
|
||||
<figure>
|
||||
<figcaption>
|
||||
<span>U</span>
|
||||
<span>M</span>
|
||||
<span>T</span>
|
||||
<span>W</span>
|
||||
<span>R</span>
|
||||
<span>F</span>
|
||||
<span>S</span>
|
||||
</figcaption>
|
||||
<form action="/" method="get">
|
||||
{{ $slot }}
|
||||
</form>
|
||||
</figure>
|
||||
</section>
|
@ -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 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-hidden focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
|
||||
{{ $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-gray-700 hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
|
||||
|
@ -2,10 +2,12 @@
|
||||
|
||||
@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';
|
||||
? 'is-active'
|
||||
: '';
|
||||
@endphp
|
||||
|
||||
<a {{ $attributes->merge(['class' => $classes]) }}>
|
||||
{{ $slot }}
|
||||
</a>
|
||||
<li class="app-button">
|
||||
<a {{ $attributes->merge(['class' => $classes]) }}>
|
||||
{{ $slot }}
|
||||
</a>
|
||||
</li>
|
||||
|
@ -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 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-hidden focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
|
||||
{{ $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-indigo-400 text-start text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-hidden 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-hidden focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out';
|
||||
@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 px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-xs hover:bg-gray-50 focus:outline-hidden focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 transition ease-in-out duration-150']) }}>
|
||||
{{ $slot }}
|
||||
</button>
|
||||
|
@ -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' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-xs']) }}>
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
<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="bg-white overflow-hidden shadow-xs sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900">
|
||||
{{ __("You're logged in!") }}
|
||||
</div>
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
<div class="py-6">
|
||||
<div class="max-w-2xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white shadow sm:rounded-lg p-6">
|
||||
<div class="bg-white shadow-sm sm:rounded-lg p-6">
|
||||
<form method="POST"
|
||||
action="{{ $event->exists
|
||||
? route('calendars.events.update', [$calendar, $event])
|
||||
@ -38,7 +38,7 @@
|
||||
<div class="mb-6">
|
||||
<x-input-label for="description" :value="__('Description')" />
|
||||
<textarea id="description" name="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md shadow-sm border-gray-300 focus:border-indigo-300 focus:ring">{{ old('description', $event->meta?->description ?? '') }}</textarea>
|
||||
class="mt-1 block w-full rounded-md shadow-xs border-gray-300 focus:border-indigo-300 focus:ring-3">{{ old('description', $event->meta?->description ?? '') }}</textarea>
|
||||
<x-input-error class="mt-2" :messages="$errors->get('description')" />
|
||||
</div>
|
||||
|
||||
|
@ -4,30 +4,25 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="font-sans antialiased">
|
||||
<div class="min-h-screen bg-gray-100">
|
||||
@include('layouts.navigation')
|
||||
<body id="app">
|
||||
@include('layouts.navigation')
|
||||
|
||||
<!-- Page Heading -->
|
||||
<main>
|
||||
@isset($header)
|
||||
<header class="bg-white shadow">
|
||||
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||
{{ $header }}
|
||||
</div>
|
||||
</header>
|
||||
<header>
|
||||
{{ $header }}
|
||||
</header>
|
||||
@endisset
|
||||
|
||||
<!-- Page Content -->
|
||||
<main>
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
<article {{ $attributes }}>
|
||||
{{ $article ?? $slot }}
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<aside>
|
||||
@if (session('toast'))
|
||||
<div
|
||||
x-data="{ open: true }"
|
||||
@ -39,5 +34,6 @@
|
||||
{{ session('toast') }}
|
||||
</div>
|
||||
@endif
|
||||
</aside>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -4,27 +4,18 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scripts -->
|
||||
<title>{{ config('app.name', 'Kithkin') }}</title>
|
||||
@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" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
<body id="auth">
|
||||
<header>
|
||||
<a href="/">
|
||||
<x-application-logo class="logo" />
|
||||
<h1>{{ config('app.name', 'Kithkin') }}</h1>
|
||||
</a>
|
||||
</header>
|
||||
<main>
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,110 +1,64 @@
|
||||
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
|
||||
<!-- 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>
|
||||
</div>
|
||||
<nav>
|
||||
<!-- top -->
|
||||
<section class="top">
|
||||
<!-- logo -->
|
||||
<a href="{{ route('dashboard') }}">
|
||||
<x-application-logo class="logo logo--app" />
|
||||
</a>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||
{{ __('Dashboard') }}
|
||||
</x-nav-link>
|
||||
</div>
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||
<x-nav-link :href="route('calendars.index')" :active="request()->routeIs('calendars*')">
|
||||
{{ __('Calendars') }}
|
||||
</x-nav-link>
|
||||
</div>
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||
<x-nav-link :href="route('books.index')" :active="request()->routeIs('books*')">
|
||||
{{ __('Address Books') }}
|
||||
</x-nav-link>
|
||||
</div>
|
||||
</div>
|
||||
<!-- app nav -->
|
||||
<menu>
|
||||
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||
<x-icon-home class="w-7 h-7" />
|
||||
</x-nav-link>
|
||||
<x-nav-link :href="route('calendar.index')" :active="request()->routeIs('calendar*')">
|
||||
<x-icon-calendar class="w-7 h-7" />
|
||||
</x-nav-link>
|
||||
<x-nav-link :href="route('books.index')" :active="request()->routeIs('books*')">
|
||||
<x-icon-book-user class="w-7 h-7" />
|
||||
</x-nav-link>
|
||||
<menu>
|
||||
</section>
|
||||
|
||||
<!-- Settings Dropdown -->
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||
<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">
|
||||
<div>{{ Auth::user()->name }}</div>
|
||||
<!-- bottom -->
|
||||
<section class="bottom">
|
||||
<x-button.icon :href="route('settings')">
|
||||
<x-icon-settings class="w-7 h-7" />
|
||||
</x-button.icon>
|
||||
<x-dropdown align="right">
|
||||
<x-slot name="trigger">
|
||||
<x-button.icon>
|
||||
<x-icon-user-circle class="w-7 h-7" />
|
||||
</x-button.icon>
|
||||
</x-slot>
|
||||
|
||||
<div class="ms-1">
|
||||
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<x-dropdown-link :href="route('profile.edit')">
|
||||
{{ __('Profile') }}
|
||||
</x-dropdown-link>
|
||||
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
|
||||
<x-dropdown-link :href="route('logout')"
|
||||
onclick="event.preventDefault();
|
||||
this.closest('form').submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-dropdown-link>
|
||||
</form>
|
||||
</x-slot>
|
||||
</x-dropdown>
|
||||
</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">
|
||||
<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" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Navigation Menu -->
|
||||
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
||||
<div class="pt-2 pb-3 space-y-1">
|
||||
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||
{{ __('Dashboard') }}
|
||||
</x-responsive-nav-link>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Settings Options -->
|
||||
<div class="pt-4 pb-1 border-t border-gray-200">
|
||||
<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>
|
||||
|
||||
<div class="mt-3 space-y-1">
|
||||
<x-responsive-nav-link :href="route('profile.edit')">
|
||||
<x-slot name="content">
|
||||
<div>{{ Auth::user()->name }}</div>
|
||||
<x-dropdown-link :href="route('profile.edit')">
|
||||
{{ __('Profile') }}
|
||||
</x-responsive-nav-link>
|
||||
</x-dropdown-link>
|
||||
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
|
||||
<x-responsive-nav-link :href="route('logout')"
|
||||
<x-dropdown-link :href="route('logout')"
|
||||
onclick="event.preventDefault();
|
||||
this.closest('form').submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-responsive-nav-link>
|
||||
</x-dropdown-link>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-dropdown>
|
||||
</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-hidden focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
|
||||
<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" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
@ -7,19 +7,19 @@
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
||||
<div class="p-4 sm:p-8 bg-white shadow-sm sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
@include('profile.partials.update-profile-information-form')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
||||
<div class="p-4 sm:p-8 bg-white shadow-sm sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
@include('profile.partials.update-password-form')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
||||
<div class="p-4 sm:p-8 bg-white shadow-sm sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
@include('profile.partials.delete-user-form')
|
||||
</div>
|
||||
|
@ -33,7 +33,7 @@
|
||||
<p class="text-sm mt-2 text-gray-800">
|
||||
{{ __('Your email address is unverified.') }}
|
||||
|
||||
<button form="send-verification" 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">
|
||||
<button form="send-verification" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
{{ __('Click here to re-send the verification email.') }}
|
||||
</button>
|
||||
</p>
|
||||
|
@ -28,6 +28,10 @@ Route::view('/dashboard', 'dashboard')
|
||||
->middleware(['auth', 'verified'])
|
||||
->name('dashboard');
|
||||
|
||||
Route::view('/settings', 'settings')
|
||||
->middleware(['auth', 'verified'])
|
||||
->name('settings');
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
/* User profile (generated by Breeze) */
|
||||
Route::get ('/profile', [ProfileController::class, 'edit' ])->name('profile.edit');
|
||||
@ -35,11 +39,11 @@ Route::middleware('auth')->group(function () {
|
||||
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
||||
|
||||
/* Calendars CRUD */
|
||||
Route::resource('calendars', CalendarController::class);
|
||||
Route::resource('calendar', CalendarController::class);
|
||||
|
||||
/* Nested Events CRUD */
|
||||
Route::prefix('calendars/{calendar}')
|
||||
->name('calendars.')
|
||||
Route::prefix('calendar/{calendar}')
|
||||
->name('calendar.')
|
||||
->group(function () {
|
||||
Route::get ('events/create', [EventController::class, 'create'])->name('events.create');
|
||||
Route::post('events', [EventController::class, 'store' ])->name('events.store');
|
||||
|
@ -1,21 +0,0 @@
|
||||
import defaultTheme from 'tailwindcss/defaultTheme';
|
||||
import forms from '@tailwindcss/forms';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
|
||||
'./storage/framework/views/*.php',
|
||||
'./resources/views/**/*.blade.php',
|
||||
],
|
||||
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Figtree', ...defaultTheme.fontFamily.sans],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
plugins: [forms],
|
||||
};
|