Adds calendar settings page and handling, adds new calendar subscription functionality, fixes big calendar object for display, improvements to object
This commit is contained in:
parent
e79476fa85
commit
98fb10bc14
@ -12,6 +12,7 @@ use App\Models\CalendarMeta;
|
||||
use App\Models\CalendarInstance;
|
||||
use App\Models\Event;
|
||||
use App\Models\EventMeta;
|
||||
use App\Models\Subscription;
|
||||
|
||||
class CalendarController extends Controller
|
||||
{
|
||||
@ -44,26 +45,47 @@ class CalendarController extends Controller
|
||||
// get the user's visible calendars from the left bar
|
||||
$visible = collect($request->query('c', []));
|
||||
|
||||
// load the user's calendars
|
||||
$calendars = Calendar::query()
|
||||
// load the user's local calendars
|
||||
$locals = Calendar::query()
|
||||
->select(
|
||||
'calendars.id',
|
||||
'ci.displayname',
|
||||
'ci.calendarcolor',
|
||||
'ci.uri as slug',
|
||||
'ci.timezone as timezone',
|
||||
'meta.color as meta_color',
|
||||
'meta.color_fg as meta_color_fg'
|
||||
'ci.uri as slug',
|
||||
'ci.timezone as timezone',
|
||||
'meta.color as meta_color',
|
||||
'meta.color_fg as meta_color_fg',
|
||||
DB::raw('false as is_remote')
|
||||
)
|
||||
->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')
|
||||
->get()
|
||||
->map(function ($cal) use ($visible) {
|
||||
$cal->visible = $visible->isEmpty() || $visible->contains($cal->slug);
|
||||
return $cal;
|
||||
});
|
||||
->get();
|
||||
|
||||
// load the users remote/subscription calendars
|
||||
$remotes = Subscription::query()
|
||||
->join('calendar_meta as m', 'm.subscription_id', '=', 'calendarsubscriptions.id')
|
||||
->where('principaluri', $principal)
|
||||
->orderBy('displayname')
|
||||
->select(
|
||||
'calendarsubscriptions.id',
|
||||
'calendarsubscriptions.displayname',
|
||||
'calendarsubscriptions.calendarcolor',
|
||||
'calendarsubscriptions.uri as slug',
|
||||
DB::raw('NULL as timezone'),
|
||||
'm.color as meta_color',
|
||||
'm.color_fg as meta_color_fg',
|
||||
DB::raw('true as is_remote')
|
||||
)
|
||||
->get();
|
||||
|
||||
// merge local and remote, and add the visibility flag
|
||||
$visible = collect($request->query('c', []));
|
||||
$calendars = $locals->merge($remotes)->map(function ($cal) use ($visible) {
|
||||
$cal->visible = $visible->isEmpty() || $visible->contains($cal->slug);
|
||||
return $cal;
|
||||
});
|
||||
|
||||
// handy lookup: [id => calendar row]
|
||||
$calendar_map = $calendars->keyBy('id');
|
||||
@ -101,9 +123,22 @@ class CalendarController extends Controller
|
||||
'end_ui' => optional($end_local)->format('g:ia'),
|
||||
'timezone' => $timezone,
|
||||
'visible' => $cal->visible,
|
||||
'color' => $cal->meta_color ?? $cal->calendarcolor ?? '#1a1a1a',
|
||||
'color_fg' => $cal->meta_color_fg ?? '#ffffff',
|
||||
];
|
||||
})->keyBy('id');
|
||||
|
||||
// create the mini calendar grid based on the mini cal controls
|
||||
$mini_anchor = $request->query('mini', $range['start']->toDateString());
|
||||
$mini_start = Carbon::parse($mini_anchor)->startOfMonth();
|
||||
$mini_nav = [
|
||||
'prev' => $mini_start->copy()->subMonth()->toDateString(),
|
||||
'next' => $mini_start->copy()->addMonth()->toDateString(),
|
||||
'today' => Carbon::today()->startOfMonth()->toDateString(),
|
||||
'label' => $mini_start->format('F Y'),
|
||||
];
|
||||
$mini = $this->buildMiniGrid($mini_start, $events);
|
||||
|
||||
// create the calendar grid of days
|
||||
$grid = $this->buildCalendarGrid($view, $range, $events);
|
||||
|
||||
@ -121,18 +156,23 @@ class CalendarController extends Controller
|
||||
'month' => $range['start']->format("F"),
|
||||
'day' => $range['start']->format("d"),
|
||||
],
|
||||
'calendars' => $calendar_map->map(function ($cal) {
|
||||
'calendars' => $calendars->mapWithKeys(function ($cal) {
|
||||
return [
|
||||
'id' => $cal->id,
|
||||
'slug' => $cal->slug,
|
||||
'name' => $cal->displayname,
|
||||
'color' => $cal->meta_color ?? $cal->calendarcolor ?? '#1a1a1a', // clean this up @todo
|
||||
'color_fg' => $cal->meta_color_fg ?? '#ffffff', // clean this up
|
||||
'visible' => true, // default to visible; the UI can toggle this
|
||||
$cal->id => [
|
||||
'id' => $cal->id,
|
||||
'slug' => $cal->slug,
|
||||
'name' => $cal->displayname,
|
||||
'color' => $cal->meta_color ?? $cal->calendarcolor ?? '#1a1a1a',
|
||||
'color_fg' => $cal->meta_color_fg ?? '#ffffff',
|
||||
'visible' => $cal->visible,
|
||||
'is_remote' => $cal->is_remote,
|
||||
],
|
||||
];
|
||||
}),
|
||||
'events' => $events, // keyed, one copy each
|
||||
'grid' => $grid, // day objects hold only ID-sets
|
||||
'events' => $events, // keyed, one copy each
|
||||
'grid' => $grid, // day objects hold only ID-sets
|
||||
'mini' => $mini, // mini calendar days with events for indicators
|
||||
'mini_nav' => $mini_nav, // separate mini calendar navigation
|
||||
];
|
||||
|
||||
return view('calendar.index', $payload);
|
||||
@ -386,4 +426,54 @@ class CalendarController extends Controller
|
||||
? ['days' => $days, 'weeks' => array_chunk($days, 7)]
|
||||
: ['days' => $days];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the mini-month grid for day buttons
|
||||
*
|
||||
* Returns ['days' => [
|
||||
* [
|
||||
* 'date' => '2025-06-30',
|
||||
* 'label' => '30',
|
||||
* 'in_month' => false,
|
||||
* 'events' => [id, id …]
|
||||
* ], …
|
||||
* ]]
|
||||
*/
|
||||
private function buildMiniGrid(Carbon $monthStart, Collection $events): array
|
||||
{
|
||||
// get bounds
|
||||
$monthEnd = $monthStart->copy()->endOfMonth();
|
||||
$gridStart = $monthStart->copy()->startOfWeek(Carbon::MONDAY);
|
||||
$gridEnd = $monthEnd->copy()->endOfWeek(Carbon::SUNDAY);
|
||||
|
||||
// ensure we have 42 days (6 rows); 35 = add one extra week
|
||||
if ($gridStart->diffInDays($gridEnd) + 1 < 42) {
|
||||
$gridEnd->addWeek();
|
||||
}
|
||||
|
||||
/* map event-ids by yyyy-mm-dd */
|
||||
$byDay = [];
|
||||
foreach ($events as $ev) {
|
||||
$s = Carbon::parse($ev['start']);
|
||||
$e = $ev['end'] ? Carbon::parse($ev['end']) : $s;
|
||||
for ($d = $s->copy()->startOfDay(); $d->lte($e); $d->addDay()) {
|
||||
$byDay[$d->toDateString()][] = $ev['id'];
|
||||
}
|
||||
}
|
||||
|
||||
/* Walk the 42-day span */
|
||||
$days = [];
|
||||
for ($d = $gridStart->copy(); $d->lte($gridEnd); $d->addDay()) {
|
||||
$iso = $d->toDateString();
|
||||
$days[] = [
|
||||
'date' => $iso,
|
||||
'label' => $d->format('j'),
|
||||
'in_month' => $d->between($monthStart, $monthEnd),
|
||||
'events' => $byDay[$iso] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
// will always be 42 to ensure 6 rows
|
||||
return ['days' => $days];
|
||||
}
|
||||
}
|
||||
|
62
app/Http/Controllers/CalendarSettingsController.php
Normal file
62
app/Http/Controllers/CalendarSettingsController.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CalendarSettingsController extends Controller
|
||||
{
|
||||
/* landing page – list of settings choices */
|
||||
public function index()
|
||||
{
|
||||
return $this->frame('calendar.settings.subscribe');
|
||||
}
|
||||
|
||||
/* show “Subscribe to a calendar” form */
|
||||
public function subscribeForm()
|
||||
{
|
||||
return $this->frame(
|
||||
'calendar.settings.subscribe',
|
||||
[
|
||||
'title' => 'Subscribe to a calendar',
|
||||
'sub' => 'Add an `.ics` calender from another service'
|
||||
]);
|
||||
}
|
||||
|
||||
/* handle POST from the subscribe form */
|
||||
public function subscribeStore(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'source' => ['required', 'url'],
|
||||
'displayname' => ['nullable', 'string', 'max:100'],
|
||||
'color' => ['nullable', 'regex:/^#[0-9A-F]{6}$/i'],
|
||||
]);
|
||||
|
||||
DB::table('calendarsubscriptions')->insert([
|
||||
'uri' => Str::uuid(), // local id
|
||||
'principaluri' => 'principals/'.$request->user()->email,
|
||||
'source' => $data['source'],
|
||||
'displayname' => $data['displayname'] ?: $data['source'],
|
||||
'calendarcolor' => $data['color'],
|
||||
'refreshrate' => 'P1D', // daily
|
||||
'lastmodified' => now()->timestamp,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('calendar.settings')
|
||||
->with('toast', __('Subscription added successfully!'));
|
||||
}
|
||||
|
||||
/**
|
||||
* content frame handler
|
||||
*/
|
||||
private function frame(?string $view = null, array $data = [])
|
||||
{
|
||||
return view('calendar.settings.index', [
|
||||
'view' => $view,
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
}
|
@ -32,21 +32,11 @@ class SubscriptionController extends Controller
|
||||
'refreshrate' => 'nullable|string|max:10',
|
||||
]);
|
||||
|
||||
Subscription::create([
|
||||
'uri' => Str::uuid(), // unique per principal
|
||||
'principaluri' => auth()->user()->principal_uri,
|
||||
//...$data,
|
||||
'source' => $data['source'],
|
||||
'displayname' => $data['displayname'] ?? null,
|
||||
'calendarcolor' => $data['calendarcolor'] ?? null,
|
||||
'refreshrate' => $data['refreshrate'] ?? null,
|
||||
'striptodos' => false,
|
||||
'stripalarms' => false,
|
||||
'stripattachments' => false,
|
||||
]);
|
||||
Subscription::createWithMeta($request->user(), $data);
|
||||
|
||||
return redirect()->route('subscription.index')
|
||||
->with('toast', __('Subscription added!'));
|
||||
return redirect()
|
||||
->route('calendar.index')
|
||||
->with('toast', __('Subscription added!'));
|
||||
}
|
||||
|
||||
public function edit(Subscription $subscription)
|
||||
@ -68,8 +58,22 @@ class SubscriptionController extends Controller
|
||||
'stripattachments' => 'sometimes|boolean',
|
||||
]);
|
||||
|
||||
// update calendarsubscriptions record
|
||||
$subscription->update($data);
|
||||
|
||||
// update corresponding calendar_meta record
|
||||
$subscription->meta()->updateOrCreate(
|
||||
[], // no “where” clause → look at subscription_id FK
|
||||
[
|
||||
'title' => $subscription->displayname,
|
||||
'color' => $subscription->calendarcolor ?? '#1a1a1a',
|
||||
'color_fg' => contrast_text_color(
|
||||
$subscription->calendarcolor ?? '#1a1a1a'
|
||||
),
|
||||
'updated_at'=> now(),
|
||||
]
|
||||
);
|
||||
|
||||
return back()->with('toast', __('Subscription updated!'));
|
||||
}
|
||||
|
||||
|
29
app/Http/Middleware/HtmxAwareAuthenticate.php
Normal file
29
app/Http/Middleware/HtmxAwareAuthenticate.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Auth\Middleware\Authenticate as Base;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HtmxAwareAuthenticate extends Base
|
||||
{
|
||||
protected function redirectTo(Request $request): ?string
|
||||
{
|
||||
// Non-HTMX: fall back to normal redirect
|
||||
return $request->isMethod('get') ? route('login') : null;
|
||||
}
|
||||
|
||||
protected function unauthenticated($request, array $guards)
|
||||
{
|
||||
// If it was an HTMX request, send 401 + HX-Redirect instead of 302
|
||||
if ($request->header('HX-Request')) {
|
||||
abort(
|
||||
response('')
|
||||
->header('HX-Redirect', route('login'))
|
||||
->setStatusCode(401)
|
||||
);
|
||||
}
|
||||
|
||||
parent::unauthenticated($request, $guards);
|
||||
}
|
||||
}
|
@ -3,11 +3,15 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\User;
|
||||
|
||||
class Subscription extends Model
|
||||
{
|
||||
protected $table = 'calendarsubscriptions';
|
||||
public $timestamps = false; // sabre table without created_at/updated_at; may need a meta table?
|
||||
/** basic table mapping */
|
||||
protected $table = 'calendarsubscriptions';
|
||||
public $timestamps = false; // sabre table
|
||||
|
||||
protected $fillable = [
|
||||
'uri',
|
||||
@ -22,10 +26,54 @@ class Subscription extends Model
|
||||
'stripattachments',
|
||||
];
|
||||
|
||||
/** Cast tinyint columns to booleans */
|
||||
protected $casts = [
|
||||
'striptodos' => 'bool',
|
||||
'stripalarms' => 'bool',
|
||||
'stripattachments' => 'bool',
|
||||
];
|
||||
|
||||
/** relationship to meta row */
|
||||
public function meta()
|
||||
{
|
||||
return $this->hasOne(\App\Models\CalendarMeta::class,
|
||||
'subscription_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a remote calendar subscription and its UI metadata (calendar_meta).
|
||||
*/
|
||||
public static function createWithMeta(User $user, array $data): self
|
||||
{
|
||||
return DB::transaction(function () use ($user, $data) {
|
||||
|
||||
/** insert into calendarsubscriptions */
|
||||
$sub = self::create([
|
||||
'uri' => (string) Str::uuid(),
|
||||
'principaluri' => $user->principal_uri,
|
||||
'source' => $data['source'],
|
||||
'displayname' => $data['displayname'] ?? $data['source'],
|
||||
'calendarcolor' => $data['calendarcolor'] ?? null,
|
||||
'refreshrate' => $data['refreshrate'] ?? 'P1D',
|
||||
'calendarorder' => 0,
|
||||
'striptodos' => false,
|
||||
'stripalarms' => false,
|
||||
'stripattachments' => false,
|
||||
'lastmodified' => now()->timestamp,
|
||||
]);
|
||||
|
||||
/** create corresponding calendar_meta row */
|
||||
DB::table('calendar_meta')->insert([
|
||||
'subscription_id' => $sub->id,
|
||||
'is_remote' => true,
|
||||
'title' => $sub->displayname,
|
||||
'color' => $sub->calendarcolor ?? '#1a1a1a',
|
||||
'color_fg' => contrast_text_color($sub->calendarcolor ?? '#1a1a1a'),
|
||||
'is_shared' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return $sub;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
10
composer.lock
generated
10
composer.lock
generated
@ -1137,16 +1137,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v12.20.0",
|
||||
"version": "v12.21.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/framework.git",
|
||||
"reference": "1b9a00f8caf5503c92aa436279172beae1a484ff"
|
||||
"reference": "ac8c4e73bf1b5387b709f7736d41427e6af1c93b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/1b9a00f8caf5503c92aa436279172beae1a484ff",
|
||||
"reference": "1b9a00f8caf5503c92aa436279172beae1a484ff",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/ac8c4e73bf1b5387b709f7736d41427e6af1c93b",
|
||||
"reference": "ac8c4e73bf1b5387b709f7736d41427e6af1c93b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -1348,7 +1348,7 @@
|
||||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2025-07-08T15:02:21+00:00"
|
||||
"time": "2025-07-22T15:41:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/prompts",
|
||||
|
@ -10,20 +10,27 @@ return new class extends Migration
|
||||
{
|
||||
Schema::create('calendar_meta', function (Blueprint $table) {
|
||||
|
||||
// // FK = PK to Sabre’s calendars.id
|
||||
$table->unsignedInteger('calendar_id')->primary(); // FK = PK
|
||||
/** keys */
|
||||
$table->id(); // primary key
|
||||
$table->unsignedInteger('calendar_id')->nullable()->unique();
|
||||
$table->unsignedInteger('subscription_id')->nullable()->unique();
|
||||
|
||||
// UI fields
|
||||
$table->string('title')->nullable(); // ui override
|
||||
$table->string('color', 7)->nullable(); // bg color
|
||||
$table->string('color_fg', 7)->nullable(); // fg color
|
||||
/** ui-specific fields */
|
||||
$table->string('title')->nullable(); // override name
|
||||
$table->string('color', 7)->nullable(); // background
|
||||
$table->string('color_fg', 7)->nullable(); // foreground
|
||||
$table->boolean('is_shared')->default(false);
|
||||
$table->json('settings')->nullable(); // arbitrary JSON
|
||||
$table->boolean('is_remote')->default(false); // local vs. subscription
|
||||
$table->json('settings')->nullable(); // arbitrary JSON
|
||||
$table->timestamps();
|
||||
|
||||
/** foreign-key constraints */
|
||||
$table->foreign('calendar_id')
|
||||
->references('id')
|
||||
->on('calendars')
|
||||
->references('id')->on('calendars')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->foreign('subscription_id')
|
||||
->references('id')->on('calendarsubscriptions')
|
||||
->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
@ -84,7 +84,8 @@ body {
|
||||
* section
|
||||
*/
|
||||
main {
|
||||
@apply rounded-lg bg-white;
|
||||
@apply overflow-hidden rounded-lg;
|
||||
max-height: calc(100dvh - 1rem);
|
||||
|
||||
/* app */
|
||||
body#app & {
|
||||
@ -98,37 +99,76 @@ main {
|
||||
|
||||
/* auth screens */
|
||||
body#auth & {
|
||||
@apply w-1/2 mx-auto p-8;
|
||||
@apply bg-white border-md border-primary shadow-drop w-1/2 mx-auto p-8 rounded-xl;
|
||||
min-width: 16rem;
|
||||
max-width: 40rem;
|
||||
}
|
||||
|
||||
/* left column */
|
||||
aside {
|
||||
@apply flex flex-col col-span-1 px-6 2xl:px-8 pb-8 h-full;
|
||||
@apply bg-white flex flex-col col-span-1 pb-8 h-full rounded-l-lg;
|
||||
@apply overflow-y-auto;
|
||||
|
||||
> h1 {
|
||||
@apply flex items-center h-20;
|
||||
@apply flex items-center h-20 min-h-20 px-6 2xl:px-8;
|
||||
@apply backdrop-blur-xs sticky top-0 z-1 shrink-0;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
> .aside-inset {
|
||||
@apply px-6 2xl:px-8;
|
||||
}
|
||||
|
||||
.content {
|
||||
@apply grow;
|
||||
}
|
||||
|
||||
.drawers {
|
||||
@apply grow flex flex-col gap-6 pt-1;
|
||||
}
|
||||
|
||||
.pagelinks {
|
||||
@apply flex flex-col gap-2px -mx-3;
|
||||
width: calc(100% + 1.5rem);
|
||||
|
||||
a {
|
||||
@apply flex flex-row gap-2 items-center justify-start px-3 h-9 rounded-md;
|
||||
transition: background-color 100ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
@apply bg-cyan-200;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
@apply bg-cyan-400;
|
||||
|
||||
&:hover {
|
||||
@apply bg-cyan-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* main content wrapper */
|
||||
article {
|
||||
@apply grid grid-cols-1 w-full pr-6 2xl:pr-8;
|
||||
@apply bg-white grid grid-cols-1 w-full pl-3 2xl:pl-4 pr-6 2xl:pr-8 rounded-r-lg;
|
||||
@apply overflow-y-auto;
|
||||
grid-template-rows: 5rem auto;
|
||||
|
||||
/* main content title and actions */
|
||||
> header {
|
||||
@apply flex flex-row items-center justify-between w-full;
|
||||
@apply bg-white sticky top-0;
|
||||
|
||||
/* if h1 exists it means there's no aside, so force the width from that */
|
||||
h1 {
|
||||
@apply flex items-center pl-6 2xl:pl-8;
|
||||
@apply relative flex items-center pl-6 2xl:pl-8;
|
||||
width: minmax(20rem, 20dvw);
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply flex flex-row gap-1 items-center justify-start relative top-px;
|
||||
@apply flex flex-row gap-1 items-center justify-start relative top-2px;
|
||||
|
||||
> span {
|
||||
@apply text-gray-700;
|
||||
@ -139,6 +179,27 @@ main {
|
||||
@apply flex flex-row items-center justify-end gap-4;
|
||||
}
|
||||
}
|
||||
|
||||
/* sections with max width content */
|
||||
&.readable {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
|
||||
> header {
|
||||
@apply col-span-4;
|
||||
}
|
||||
|
||||
> .content {
|
||||
@apply col-span-4 2xl:col-span-3;
|
||||
}
|
||||
}
|
||||
|
||||
/* section specific */
|
||||
&#calendar {
|
||||
/* */
|
||||
}
|
||||
&#settings {
|
||||
/* */
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (width >= 96rem) { /* 2xl */
|
||||
|
@ -61,7 +61,9 @@
|
||||
|
||||
--text-2xs: 0.625rem;
|
||||
--text-2xs--line-height: 1.3;
|
||||
--text-xs : 0.8rem;
|
||||
--text-xs: 0.8rem;
|
||||
--text-md: 1.075rem;
|
||||
--text-md--line-height: 1.4;
|
||||
--text-2xl: 1.75rem;
|
||||
--text-2xl--line-height: 1.333;
|
||||
--text-3xl: 2rem;
|
||||
|
@ -46,3 +46,16 @@ a {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* text */
|
||||
p {
|
||||
@apply text-base leading-normal;
|
||||
|
||||
&.big {
|
||||
@apply text-lg font-medium;
|
||||
}
|
||||
}
|
||||
|
||||
.description { /* contains <p> and uses gap for spacing */
|
||||
@apply space-y-3;
|
||||
}
|
||||
|
@ -6,12 +6,12 @@ button,
|
||||
--button-accent: var(--color-primary-hover);
|
||||
|
||||
&.button--primary {
|
||||
@apply bg-cyan-300 border-md border-solid;
|
||||
@apply bg-cyan-400 border-md border-solid;
|
||||
border-color: var(--button-border);
|
||||
box-shadow: 2.5px 2.5px 0 0 var(--button-border);
|
||||
|
||||
&:hover {
|
||||
@apply bg-cyan-400;
|
||||
@apply bg-cyan-500;
|
||||
border-color: var(--button-accent);
|
||||
}
|
||||
|
||||
@ -30,6 +30,10 @@ button,
|
||||
background-color: rgba(0,0,0,0.075);
|
||||
}
|
||||
}
|
||||
|
||||
&.button--sm {
|
||||
@apply text-base px-2 h-8;
|
||||
}
|
||||
}
|
||||
|
||||
/* button groups are used with labels/checks as well as regular buttons */
|
||||
@ -49,16 +53,20 @@ button,
|
||||
|
||||
&:has(input:checked),
|
||||
&:active {
|
||||
@apply bg-cyan-300 border-r-transparent;
|
||||
@apply bg-cyan-400 border-r-transparent;
|
||||
box-shadow:
|
||||
inset 2.5px 0 0 0 var(--color-primary),
|
||||
inset 0 0.25rem 0 0 var(--color-cyan-400);
|
||||
inset 0 0.25rem 0 0 var(--color-cyan-500);
|
||||
top: 2.5px;
|
||||
|
||||
+ label,
|
||||
+ button {
|
||||
box-shadow: inset 1.5px 0 0 0 var(--color-primary);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-cyan-500;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
@ -66,7 +74,7 @@ button,
|
||||
|
||||
&:has(input:checked),
|
||||
&:active {
|
||||
box-shadow: inset 0 0.25rem 0 0 var(--color-cyan-400);
|
||||
box-shadow: inset 0 0.25rem 0 0 var(--color-cyan-500);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,3 +14,7 @@ input[type="checkbox"] {
|
||||
box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--checkbox-color), var(--tw-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
label.checkbox-label {
|
||||
@apply flex flex-row items-center gap-2;
|
||||
}
|
||||
|
@ -1,9 +1,57 @@
|
||||
/**
|
||||
* default text inputs
|
||||
*/
|
||||
input[type="email"],
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="text"],
|
||||
input[type="url"],
|
||||
input[type="search"] {
|
||||
@apply border-md border-gray-800 bg-white rounded-md shadow-input;
|
||||
@apply focus:border-primary focus:ring-2 focus:ring-offset-2 focus:ring-cyan-600;
|
||||
transition: box-shadow 125ms ease-in-out,
|
||||
border-color 125ms ease-in-out;
|
||||
}
|
||||
|
||||
/**
|
||||
* specific minor types
|
||||
*/
|
||||
input[type="color"] {
|
||||
@apply rounded-md border-white shadow-none h-10 w-16 cursor-pointer;
|
||||
@apply self-start;
|
||||
|
||||
&::-moz-color-swatch {
|
||||
@apply border-none;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* labeled text inputs
|
||||
*/
|
||||
label.text-label {
|
||||
@apply flex flex-col gap-2;
|
||||
|
||||
> .label {
|
||||
@apply font-semibold text-md;
|
||||
}
|
||||
|
||||
> .description {
|
||||
@apply text-gray-800;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* form layouts
|
||||
*/
|
||||
.form-grid-1 {
|
||||
@apply grid grid-cols-3 gap-6;
|
||||
|
||||
> label {
|
||||
@apply col-span-2 col-start-1;
|
||||
}
|
||||
|
||||
> div {
|
||||
@apply col-span-3 flex flex-row justify-start gap-4 pt-4;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -3,15 +3,19 @@
|
||||
|
||||
/* mini controls */
|
||||
header{
|
||||
@apply flex items-center justify-between px-2 pb-3;
|
||||
@apply flex flex-row items-center justify-between px-2 pb-2;
|
||||
|
||||
> span {
|
||||
@apply font-serif text-lg;
|
||||
@apply font-serif text-lg grow;
|
||||
}
|
||||
|
||||
> form {
|
||||
@apply flex flex-row items-center justify-end gap-1 shrink-0 text-sm;
|
||||
}
|
||||
}
|
||||
|
||||
/* days wrapper */
|
||||
figure {
|
||||
nav {
|
||||
@apply border-md border-primary shadow-drop rounded-md;
|
||||
|
||||
/* weekdays */
|
||||
|
1
resources/svg/icons/calendar-sync.svg
Normal file
1
resources/svg/icons/calendar-sync.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-sync-icon lucide-calendar-sync"><path d="M11 10v4h4"/><path d="m11 14 1.535-1.605a5 5 0 0 1 8 1.5"/><path d="M16 2v4"/><path d="m21 18-1.535 1.605a5 5 0 0 1-8-1.5"/><path d="M21 22v-4h-4"/><path d="M21 8.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h4.3"/><path d="M3 10h4"/><path d="M8 2v4"/></svg>
|
After Width: | Height: | Size: 515 B |
1
resources/svg/icons/globe.svg
Normal file
1
resources/svg/icons/globe.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-globe-icon lucide-globe"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>
|
After Width: | Height: | Size: 344 B |
@ -4,25 +4,24 @@
|
||||
<h1>
|
||||
{{ __('Calendar') }}
|
||||
</h1>
|
||||
<div class="grow flex flex-col gap-4">
|
||||
<details open>
|
||||
<summary>{{ __('My Calendars') }}</summary>
|
||||
<form id="calendar-toggles"
|
||||
class="content"
|
||||
<div class="content aside-inset">
|
||||
<form id="calendar-toggles"
|
||||
class="drawers"
|
||||
action="{{ route('calendar.index') }}"
|
||||
method="get">
|
||||
<ul>
|
||||
@foreach ($calendars as $cal)
|
||||
<details open>
|
||||
<summary>{{ __('My Calendars') }}</summary>
|
||||
<ul class="content">
|
||||
@foreach ($calendars->where('is_remote', false) as $cal)
|
||||
<li>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input type="checkbox"
|
||||
class="calendar-toggle"
|
||||
name="c[]"
|
||||
value="{{ $cal['slug'] }}"
|
||||
style="--checkbox-color: {{ $cal['color'] }}"
|
||||
@checked($cal['visible'])>
|
||||
<span>{{ $cal['name'] }}</span>
|
||||
</label>
|
||||
<x-input.checkbox-label
|
||||
label="{{ $cal['name'] }}"
|
||||
name="c[]"
|
||||
value="{{ $cal['slug'] }}"
|
||||
:checked="$cal['visible']"
|
||||
checkclass="calendar-toggle"
|
||||
style="--checkbox-color: {{ $cal['color'] }}"
|
||||
/>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@ -31,16 +30,31 @@
|
||||
<noscript>
|
||||
<button type="submit">{{ __('Apply') }}</button>
|
||||
</noscript>
|
||||
</form>
|
||||
</details>
|
||||
</details>
|
||||
<details open>
|
||||
<summary>
|
||||
<span>{{ __('Other Calendars') }}</span>
|
||||
<a href="{{ route('calendar.settings.subscribe') }}"
|
||||
class="button button--icon button--sm">+</a>
|
||||
</summary>
|
||||
<ul class="content">
|
||||
@foreach ($calendars->where('is_remote', true) as $cal)
|
||||
<li>
|
||||
<x-input.checkbox-label
|
||||
label="{{ $cal['name'] }}"
|
||||
name="c[]"
|
||||
value="{{ $cal['slug'] }}"
|
||||
:checked="$cal['visible']"
|
||||
checkclass="calendar-toggle"
|
||||
style="--checkbox-color: {{ $cal['color'] }}"
|
||||
/>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</details>
|
||||
</form>
|
||||
</div>
|
||||
<x-calendar.mini>
|
||||
@foreach ($grid['weeks'] as $week)
|
||||
@foreach ($week as $day)
|
||||
<x-calendar.mini-day :day="$day" />
|
||||
@endforeach
|
||||
@endforeach
|
||||
</x-calendar.mini>
|
||||
<x-calendar.mini :mini="$mini" :nav="$mini_nav" :view="$view" class="aside-inset" />
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="header">
|
||||
@ -63,13 +77,13 @@
|
||||
{{-- keep current view (month/week/4day) --}}
|
||||
<input type="hidden" name="view" value="{{ $view }}">
|
||||
<nav class="button-group button-group--primary">
|
||||
<x-button.group-button type="submit" name="date" value="{{ $nav['prev'] }}">
|
||||
<x-button.group-button type="submit" name="date" value="{{ $nav['prev'] }}" aria-label="Go back 1 month">
|
||||
<x-icon-chevron-left />
|
||||
</x-button.group-button>
|
||||
<x-button.group-button type="submit" name="date" value="{{ $nav['today'] }}">
|
||||
<x-button.group-button type="submit" name="date" value="{{ $nav['today'] }}" aria-label="Go to today">
|
||||
Today
|
||||
</x-button.group-button>
|
||||
<x-button.group-button type="submit" name="date" value="{{ $nav['next'] }}">
|
||||
<x-button.group-button type="submit" name="date" value="{{ $nav['next'] }}" aria-label="Go forward 1 month">
|
||||
<x-icon-chevron-right />
|
||||
</x-button.group-button>
|
||||
</nav>
|
||||
@ -91,7 +105,7 @@
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="button button--icon" href="{{ route('calendar.create') }}">
|
||||
<a class="button button--icon" href="{{ route('calendar.settings') }}">
|
||||
<x-icon-settings />
|
||||
</a>
|
||||
</li>
|
||||
|
30
resources/views/calendar/settings/index.blade.php
Normal file
30
resources/views/calendar/settings/index.blade.php
Normal file
@ -0,0 +1,30 @@
|
||||
<x-app-layout id="settings" class="readable">
|
||||
|
||||
<x-slot name="aside">
|
||||
<h1>
|
||||
{{ __('Settings') }}
|
||||
</h1>
|
||||
<x-calendar.settings-menu />
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="header">
|
||||
<h2>
|
||||
@isset($data['title'])
|
||||
{{ $data['title'] }}
|
||||
@else
|
||||
{{ __('Settings') }}
|
||||
@endisset
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="article">
|
||||
<div class="content">
|
||||
@isset($view)
|
||||
@include($view, $data ?? [])
|
||||
@else
|
||||
<p class="text-muted">{{ __('Pick an option in the sidebar…') }}</p>
|
||||
@endisset
|
||||
<div class="content">
|
||||
</x-slot>
|
||||
|
||||
</x-app-layout>
|
33
resources/views/calendar/settings/subscribe.blade.php
Normal file
33
resources/views/calendar/settings/subscribe.blade.php
Normal file
@ -0,0 +1,33 @@
|
||||
<div class="description">
|
||||
<p>
|
||||
Subscribing to a calendar adds it to your Kithkin calendar set and syncs any changes back to the original service. This means that any changes you make here will be sent to that server, and vice versa.
|
||||
<p>
|
||||
It's possible (and likely) that some of the things you can add to your calendar here will not work in other calendar apps or services. You'll always see that data here, though.
|
||||
</p>
|
||||
</div>
|
||||
<form method="post"
|
||||
action="{{ route('calendar.settings.subscribe.store') }}"
|
||||
class="form-grid-1 mt-8">
|
||||
@csrf
|
||||
|
||||
<x-input.text-label
|
||||
name="source"
|
||||
type="url"
|
||||
label="{{ __('Calendar URL (ICS)') }}"
|
||||
placeholder="https://ical.mac.com/ical/MoonPhases.ics"
|
||||
required="true" />
|
||||
<x-input.text-label
|
||||
name="displayname"
|
||||
type="text"
|
||||
label="{{ __('Display name') }}"
|
||||
placeholder="Phases of the moon..."
|
||||
required="true"
|
||||
description="If you leave this blank, we'll try to make a best guess for the name." />
|
||||
<x-input.text-label name="color" type="color" label="{{ __('Calendar color') }}" />
|
||||
|
||||
<div class="flex gap-4">
|
||||
<x-button variant="primary" type="submit">{{ __('Subscribe') }}</x-button>
|
||||
<a href="{{ route('calendar.index') }}"
|
||||
class="button button--secondary">{{ __('Cancel and go back') }}</a>
|
||||
</div>
|
||||
</form>
|
11
resources/views/components/app/pagelink.blade.php
Normal file
11
resources/views/components/app/pagelink.blade.php
Normal file
@ -0,0 +1,11 @@
|
||||
@props(['active'])
|
||||
|
||||
@php
|
||||
$classes = ($active ?? false)
|
||||
? 'is-active'
|
||||
: '';
|
||||
@endphp
|
||||
|
||||
<a {{ $attributes->merge(['class' => $classes]) }}>
|
||||
{{ $slot }}
|
||||
</a>
|
@ -17,11 +17,8 @@
|
||||
@if(!empty($day['events']))
|
||||
@foreach (array_keys($day['events']) as $eventId)
|
||||
@php
|
||||
/* pull the full event once */
|
||||
$event = $events[$eventId];
|
||||
|
||||
/* calendar color */
|
||||
$bg = $calendars[$event['calendar_id']]['color'] ?? '#999';
|
||||
$color = $event['color'] ?? '#999';
|
||||
@endphp
|
||||
<a class="event{{ $event['visible'] ? '' : ' hidden' }}"
|
||||
href="{{ route('calendar.events.show', [$event['calendar_slug'], $event['id']]) }}"
|
||||
@ -29,7 +26,7 @@
|
||||
hx-target="#modal"
|
||||
hx-push-url="false"
|
||||
hx-swap="innerHTML"
|
||||
style="--event-color: {{ $bg }}"
|
||||
style="--event-color: {{ $color }}"
|
||||
data-calendar="{{ $event['calendar_slug'] }}">
|
||||
<i class="indicator" aria-label="Calendar indicator"></i>
|
||||
<span class="title">{{ $event['title'] }}</span>
|
||||
|
@ -1,11 +1,31 @@
|
||||
@props(['class' => ''])
|
||||
@props(['mini', 'view', 'nav', 'class' => ''])
|
||||
|
||||
<section class="mini mini--month {{ $class }}">
|
||||
<section id="mini" class="mini mini--month {{ $class }}">
|
||||
<header>
|
||||
<span>July 2025</span>
|
||||
<menu>Controls</menu>
|
||||
<span>{{ $nav['label'] }}</span>
|
||||
<form
|
||||
action="{{ route('calendar.index') }}"
|
||||
method="get"
|
||||
class="flex flex-row gap-1 items-center"
|
||||
hx-get="{{ route('calendar.index') }}"
|
||||
hx-target="#mini"
|
||||
hx-select="#mini"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true">
|
||||
{{-- preserve main calendar context for full-reload fallback --}}
|
||||
<input type="hidden" name="view" value="{{ $view }}">
|
||||
<input type="hidden" name="date" value="{{ request('date') }}">
|
||||
{{-- nav buttons --}}
|
||||
<button type="submit" name="mini" class="button--icon button--sm" value="{{ $nav['prev'] }}" aria-label="Go back 1 month">
|
||||
<x-icon-chevron-left />
|
||||
</button>
|
||||
{{-- <!--<button type="submit" name="mini" class="button--sm" value="{{ $nav['today'] }}">Today</button>--> --}}
|
||||
<button type="submit" name="mini" class="button--icon button--sm" value="{{ $nav['next'] }}" aria-label="Go forward 1 month">
|
||||
<x-icon-chevron-right />
|
||||
</button>
|
||||
</form>
|
||||
</header>
|
||||
<figure>
|
||||
<nav>
|
||||
<hgroup>
|
||||
<span>Mo</span>
|
||||
<span>Tu</span>
|
||||
@ -15,8 +35,32 @@
|
||||
<span>Sa</span>
|
||||
<span>Su</span>
|
||||
</hgroup>
|
||||
<form action="/" method="get">
|
||||
{{ $slot }}
|
||||
{{-- form drives the main calendar --}}
|
||||
<form action="{{ route('calendar.index') }}"
|
||||
method="get"
|
||||
hx-get="{{ route('calendar.index') }}"
|
||||
hx-target="#calendar"
|
||||
hx-select="#calendar"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
hx-include="#calendar-toggles">
|
||||
|
||||
{{-- stay on the same view (month / week…) --}}
|
||||
<input type="hidden" name="view" value="{{ $view }}">
|
||||
|
||||
@foreach ($mini['days'] as $day)
|
||||
<button
|
||||
type="submit"
|
||||
name="date"
|
||||
value="{{ $day['date'] }}"
|
||||
data-event-count="{{ count($day['events']) }}"
|
||||
class="day
|
||||
{{ count($day['events']) ? 'day--with-events' : '' }}
|
||||
{{ $day['in_month'] ? '' : 'day--outside' }}
|
||||
{{ $day['date'] === today()->toDateString() ? 'day--today' : '' }}">
|
||||
{{ $day['label'] }}
|
||||
</button>
|
||||
@endforeach
|
||||
</form>
|
||||
</figure>
|
||||
</nav>
|
||||
</section>
|
||||
|
28
resources/views/components/calendar/settings-menu.blade.php
Normal file
28
resources/views/components/calendar/settings-menu.blade.php
Normal file
@ -0,0 +1,28 @@
|
||||
<div class="drawers aside-inset">
|
||||
<details open>
|
||||
<summary>{{ __('General settings') }}</summary>
|
||||
<menu class="content pagelinks">
|
||||
<li>
|
||||
<x-app.pagelink
|
||||
href="{{ route('calendar.settings.subscribe') }}"
|
||||
:active="request()->routeIs('calendar.settings')">
|
||||
<x-icon-globe width="20" />
|
||||
<span>Language and region</span>
|
||||
</x-app.pagelink>
|
||||
</li>
|
||||
</menu>
|
||||
</details>
|
||||
<details open>
|
||||
<summary>{{ __('Add a calendar') }}</summary>
|
||||
<menu class="content pagelinks">
|
||||
<li>
|
||||
<x-app.pagelink
|
||||
href="{{ route('calendar.settings.subscribe') }}"
|
||||
:active="request()->routeIs('calendar.settings.subscribe')">
|
||||
<x-icon-calendar-sync width="20" />
|
||||
<span>Subscribe to a calendar</span>
|
||||
</x-app.pagelink>
|
||||
</li>
|
||||
</menu>
|
||||
</details>
|
||||
</div>
|
14
resources/views/components/input/checkbox-label.blade.php
Normal file
14
resources/views/components/input/checkbox-label.blade.php
Normal file
@ -0,0 +1,14 @@
|
||||
@props([
|
||||
'label' => '', // label text
|
||||
'labelclass' => '', // extra CSS classes for the label
|
||||
'checkclass' => '', // checkbox classes
|
||||
'name' => '', // checkbox name
|
||||
'value' => '', // checkbox value
|
||||
'style' => '', // raw style string for the checkbox
|
||||
'checked' => false // true/false or truthy value
|
||||
])
|
||||
|
||||
<label {{ $attributes->class("checkbox-label $labelclass") }}>
|
||||
<x-input.checkbox :name="$name" :value="$value" :class="$checkclass" :style="$style" :checked="$checked" />
|
||||
<span>{{ $label }}</span>
|
||||
</label>
|
14
resources/views/components/input/checkbox.blade.php
Normal file
14
resources/views/components/input/checkbox.blade.php
Normal file
@ -0,0 +1,14 @@
|
||||
@props([
|
||||
'class' => '', // extra CSS classes
|
||||
'name' => '', // input name
|
||||
'value' => '', // input value
|
||||
'style' => '', // raw style string
|
||||
'checked' => false // true/false or truthy value
|
||||
])
|
||||
|
||||
<input type="checkbox"
|
||||
name="{{ $name }}"
|
||||
value="{{ $value }}"
|
||||
{{ $attributes->class($class) }}
|
||||
@if($style !== '') style="{{ $style }}" @endif
|
||||
@checked($checked) />
|
26
resources/views/components/input/text-label.blade.php
Normal file
26
resources/views/components/input/text-label.blade.php
Normal file
@ -0,0 +1,26 @@
|
||||
@props([
|
||||
'label' => '', // label text
|
||||
'labelclass' => '', // extra CSS classes for the label
|
||||
'inputclass' => '', // input classes
|
||||
'name' => '', // input name
|
||||
'type' => 'text', // input type (text, url, etc)
|
||||
'value' => '', // input value
|
||||
'placeholder' => '', // placeholder text
|
||||
'style' => '', // raw style string for the input
|
||||
'required' => false, // true/false or truthy value
|
||||
'description' => '', // optional descriptive text below the input
|
||||
])
|
||||
|
||||
<label {{ $attributes->class("text-label $labelclass") }}>
|
||||
<span class="label">{{ $label }}</span>
|
||||
<x-input.text
|
||||
:type="$type"
|
||||
:name="$name"
|
||||
:value="$value"
|
||||
:class="$inputclass"
|
||||
:placeholder="$placeholder"
|
||||
:style="$style"
|
||||
:required="$required"
|
||||
/>
|
||||
@if($description !== '')<span class="description">{{ $description}}</span>@endif
|
||||
</label>
|
17
resources/views/components/input/text.blade.php
Normal file
17
resources/views/components/input/text.blade.php
Normal file
@ -0,0 +1,17 @@
|
||||
@props([
|
||||
'class' => '', // extra CSS classes
|
||||
'type' => 'text', // input type
|
||||
'name' => '', // input name
|
||||
'value' => '', // input value
|
||||
'placeholder' => '', // placeholder text
|
||||
'style' => '', // raw style string
|
||||
'required' => false, // true/false or truthy value
|
||||
])
|
||||
|
||||
<input type="{{ $type }}"
|
||||
name="{{ $name }}"
|
||||
value="{{ $value }}"
|
||||
placeholder="{{ $placeholder }}"
|
||||
{{ $attributes->class($class) }}
|
||||
@if($style !== '') style="{{ $style }}" @endif
|
||||
@required($required) />
|
@ -14,7 +14,7 @@
|
||||
<h1>{{ config('app.name', 'Kithkin') }}</h1>
|
||||
</a>
|
||||
</header>
|
||||
<main class="border-md border-primary shadow-drop">
|
||||
<main>
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</body>
|
||||
|
@ -8,15 +8,15 @@
|
||||
|
||||
<!-- app nav -->
|
||||
<menu>
|
||||
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||
<x-app.nav-button :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-app.nav-button>
|
||||
<x-app.nav-button :href="route('calendar.index')" :active="request()->routeIs('calendar*')">
|
||||
<x-icon-calendar class="w-7 h-7" />
|
||||
</x-nav-link>
|
||||
<x-nav-link :href="route('book.index')" :active="request()->routeIs('books*')">
|
||||
</x-app.nav-button>
|
||||
<x-app.nav-button :href="route('book.index')" :active="request()->routeIs('books*')">
|
||||
<x-icon-book-user class="w-7 h-7" />
|
||||
</x-nav-link>
|
||||
</x-app.nav-button>
|
||||
<menu>
|
||||
</section>
|
||||
|
||||
|
@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use App\Http\Controllers\BookController;
|
||||
use App\Http\Controllers\CalendarController;
|
||||
use App\Http\Controllers\CalendarSettingsController;
|
||||
use App\Http\Controllers\CardController;
|
||||
use App\Http\Controllers\DavController;
|
||||
use App\Http\Controllers\EventController;
|
||||
@ -51,6 +52,14 @@ Route::middleware('auth')->group(function () {
|
||||
->prefix('calendar')
|
||||
->name('calendar.')
|
||||
->group(function () {
|
||||
|
||||
// settings landing
|
||||
Route::get('settings', [CalendarSettingsController::class, 'index'])->name('settings');
|
||||
|
||||
// settings / subscribe to a calendar
|
||||
Route::get('settings/subscribe', [CalendarSettingsController::class, 'subscribeForm'])->name('settings.subscribe');
|
||||
Route::post('settings/subscribe', [CalendarSettingsController::class, 'subscribeStore'])->name('settings.subscribe.store');
|
||||
|
||||
// remote calendar subscriptions
|
||||
Route::resource('subscriptions', SubscriptionController::class)
|
||||
->except(['show']); // index, create, store, edit, update, destroy
|
||||
|
Loading…
Reference in New Issue
Block a user