Calendar now grabs all events and formatted date ranges for display; calendar view shows mini calendar; more theme and brand updates for better glitz

This commit is contained in:
Andrew Gioia 2025-07-24 12:50:44 -04:00
parent 5970991eac
commit 643ac833ba
Signed by: andrew
GPG Key ID: FC09694A000800C8
62 changed files with 1074 additions and 5706 deletions

View File

@ -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 shouldnt
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];
}
}

View File

@ -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();
}
}

View File

@ -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
View File

@ -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
View 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',
],
];

3886
curl.html

File diff suppressed because one or more lines are too long

View File

@ -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();

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -1,6 +1,5 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
'@tailwindcss/postcss': {},
},
};

View File

@ -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);
}
}

View 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);
}
}

View 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;
}

View 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;
}

View 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);
}
}
}

View 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;
}
}
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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)>

View File

@ -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 --}}

View File

@ -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')

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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]) }}>

View File

@ -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>

View File

@ -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']) }}>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

File diff suppressed because one or more lines are too long

View File

@ -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');

View File

@ -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],
};