Fixes create calendar modal form, adds kithkin config settings, improvements to input and button styles, cleans up color default handling
This commit is contained in:
parent
6242a9772a
commit
0b82c88333
@ -13,6 +13,7 @@ use App\Models\CalendarInstance;
|
|||||||
use App\Models\Event;
|
use App\Models\Event;
|
||||||
use App\Models\EventMeta;
|
use App\Models\EventMeta;
|
||||||
use App\Models\Subscription;
|
use App\Models\Subscription;
|
||||||
|
use App\Services\Calendar\CreateCalendar;
|
||||||
|
|
||||||
class CalendarController extends Controller
|
class CalendarController extends Controller
|
||||||
{
|
{
|
||||||
@ -130,6 +131,13 @@ class CalendarController extends Controller
|
|||||||
$start_local = $start_utc->copy()->timezone($timezone);
|
$start_local = $start_utc->copy()->timezone($timezone);
|
||||||
$end_local = optional($end_utc)->copy()->timezone($timezone);
|
$end_local = optional($end_utc)->copy()->timezone($timezone);
|
||||||
|
|
||||||
|
// color handling
|
||||||
|
$color = $cal['meta_color']
|
||||||
|
?? $cal['calendarcolor']
|
||||||
|
?? default_calendar_color();
|
||||||
|
$colorFg = $cal['meta_color_fg']
|
||||||
|
?? contrast_text_color($color);
|
||||||
|
|
||||||
// return events array
|
// return events array
|
||||||
return [
|
return [
|
||||||
'id' => $e->id,
|
'id' => $e->id,
|
||||||
@ -143,8 +151,8 @@ class CalendarController extends Controller
|
|||||||
'end_ui' => optional($end_local)->format('g:ia'),
|
'end_ui' => optional($end_local)->format('g:ia'),
|
||||||
'timezone' => $timezone,
|
'timezone' => $timezone,
|
||||||
'visible' => $cal->visible,
|
'visible' => $cal->visible,
|
||||||
'color' => $cal->meta_color ?? $cal->calendarcolor ?? '#1a1a1a',
|
'color' => $color,
|
||||||
'color_fg' => $cal->meta_color_fg ?? '#ffffff',
|
'color_fg' => $colorFg,
|
||||||
];
|
];
|
||||||
})->keyBy('id');
|
})->keyBy('id');
|
||||||
|
|
||||||
@ -183,6 +191,12 @@ class CalendarController extends Controller
|
|||||||
|
|
||||||
$tz = $cal->timezone ?? config('app.timezone');
|
$tz = $cal->timezone ?? config('app.timezone');
|
||||||
|
|
||||||
|
$color = $cal->meta_color
|
||||||
|
?? $cal->calendarcolor
|
||||||
|
?? default_calendar_color();
|
||||||
|
$colorFg = $cal->meta_color_fg
|
||||||
|
?? contrast_text_color($color);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $e->id,
|
'id' => $e->id,
|
||||||
'calendar_id' => $e->calendarid,
|
'calendar_id' => $e->calendarid,
|
||||||
@ -193,8 +207,8 @@ class CalendarController extends Controller
|
|||||||
'end' => optional($end_utc)->toIso8601String(),
|
'end' => optional($end_utc)->toIso8601String(),
|
||||||
'timezone' => $tz,
|
'timezone' => $tz,
|
||||||
'visible' => $cal->visible,
|
'visible' => $cal->visible,
|
||||||
'color' => $cal->meta_color ?? $cal->calendarcolor ?? '#1a1a1a',
|
'color' => $color,
|
||||||
'color_fg' => $cal->meta_color_fg ?? '#ffffff',
|
'color_fg' => $colorFg,
|
||||||
];
|
];
|
||||||
})->keyBy('id');
|
})->keyBy('id');
|
||||||
|
|
||||||
@ -223,14 +237,22 @@ class CalendarController extends Controller
|
|||||||
'month' => $range['start']->format("F"),
|
'month' => $range['start']->format("F"),
|
||||||
'day' => $range['start']->format("d"),
|
'day' => $range['start']->format("d"),
|
||||||
],
|
],
|
||||||
'calendars' => $calendars->mapWithKeys(function ($cal) {
|
'calendars' => $calendars->mapWithKeys(function ($cal)
|
||||||
|
{
|
||||||
|
// compute colors
|
||||||
|
$color = $cal->meta_color
|
||||||
|
?? $cal->calendarcolor
|
||||||
|
?? default_calendar_color();
|
||||||
|
$colorFg = $cal->meta_color_fg
|
||||||
|
?? contrast_text_color($color);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
$cal->id => [
|
$cal->id => [
|
||||||
'id' => $cal->id,
|
'id' => $cal->id,
|
||||||
'slug' => $cal->slug,
|
'slug' => $cal->slug,
|
||||||
'name' => $cal->displayname,
|
'name' => $cal->displayname,
|
||||||
'color' => $cal->meta_color ?? $cal->calendarcolor ?? '#1a1a1a',
|
'color' => $color,
|
||||||
'color_fg' => $cal->meta_color_fg ?? '#ffffff',
|
'color_fg' => $colorFg,
|
||||||
'visible' => $cal->visible,
|
'visible' => $cal->visible,
|
||||||
'is_remote' => $cal->is_remote,
|
'is_remote' => $cal->is_remote,
|
||||||
],
|
],
|
||||||
@ -245,50 +267,26 @@ class CalendarController extends Controller
|
|||||||
return view('calendar.index', $payload);
|
return view('calendar.index', $payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create()
|
|
||||||
{
|
|
||||||
return view('calendar.create');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create sabre calendar + meta
|
* create sabre calendar + meta
|
||||||
*/
|
*/
|
||||||
public function store(Request $request)
|
public function store(Request $request, CreateCalendar $creator)
|
||||||
{
|
{
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'name' => 'required|string|max:100',
|
'name' => 'required|string|max:100',
|
||||||
'description' => 'nullable|string|max:255',
|
'description' => 'nullable|string|max:255',
|
||||||
'timezone' => 'required|string',
|
'timezone' => 'required|string|max:64',
|
||||||
'color' => 'nullable|regex:/^#[0-9A-Fa-f]{6}$/',
|
'color' => 'nullable|regex:/^#[0-9A-Fa-f]{6}$/',
|
||||||
|
'redirect' => 'nullable|string', // where to go after creating
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// update master calendar entry
|
$creator->create($request->user(), $data);
|
||||||
$calId = DB::table('calendars')->insertGetId([
|
$redirect = $data['redirect'] ?? route('calendar.index');
|
||||||
'synctoken' => 1,
|
|
||||||
'components' => 'VEVENT', // or 'VEVENT,VTODO' if you add tasks
|
|
||||||
]);
|
|
||||||
|
|
||||||
// update the calendar instance row
|
return redirect($redirect)->with('toast', [
|
||||||
$instance = CalendarInstance::create([
|
'message' => __('Calendar created!'),
|
||||||
'calendarid' => $calId,
|
'type' => 'success',
|
||||||
'principaluri' => auth()->user()->uri,
|
|
||||||
'uri' => Str::uuid(),
|
|
||||||
'displayname' => $data['name'],
|
|
||||||
'description' => $data['description'] ?? null,
|
|
||||||
'calendarcolor'=> $data['color'] ?? null,
|
|
||||||
'timezone' => $data['timezone'],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// update calendar meta
|
|
||||||
$instance->meta()->create([
|
|
||||||
'calendar_id' => $instanceId,
|
|
||||||
'color' => $data['color'] ?? '#1a1a1a',
|
|
||||||
'color_fg' => contrast_text_color($data['color'] ?? '#1a1a1a'),
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return redirect()->route('calendar.index');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -365,9 +363,10 @@ class CalendarController extends Controller
|
|||||||
$calendar->increment('synctoken');
|
$calendar->increment('synctoken');
|
||||||
|
|
||||||
// update calendar meta (our table)
|
// update calendar meta (our table)
|
||||||
|
$color = calendar_color($data);
|
||||||
$calendar->meta()->updateOrCreate([], [
|
$calendar->meta()->updateOrCreate([], [
|
||||||
'color' => $data['color'] ?? '#1a1a1a',
|
'color' => $color,
|
||||||
'color_fg' => contrast_text_color($data['color'] ?? '#1a1a1a')
|
'color_fg' => contrast_text_color($color),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
|
|||||||
@ -5,6 +5,7 @@ namespace App\Http\Controllers;
|
|||||||
use App\Models\CalendarInstance;
|
use App\Models\CalendarInstance;
|
||||||
use App\Models\CalendarMeta;
|
use App\Models\CalendarMeta;
|
||||||
use App\Models\Subscription;
|
use App\Models\Subscription;
|
||||||
|
use App\Services\Calendar\CreateCalendar;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@ -102,6 +103,51 @@ class CalendarSettingsController extends Controller
|
|||||||
->with('toast', __('Settings saved!'));
|
->with('toast', __('Settings saved!'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a local calendar
|
||||||
|
**/
|
||||||
|
|
||||||
|
public function createForm(Request $request)
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'title' => __('calendar.settings.create.title'),
|
||||||
|
'sub' => __('calendar.settings.create.subtitle'),
|
||||||
|
'defaults' => [
|
||||||
|
'name' => '',
|
||||||
|
'description' => '',
|
||||||
|
'timezone' => $user->timezone ?? config('app.timezone', 'UTC'),
|
||||||
|
'color' => default_calendar_color(),
|
||||||
|
],
|
||||||
|
'redirect' => route('calendar.settings'),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($request->header('HX-Request')) {
|
||||||
|
return view('calendar.partials.create-modal', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->frame('calendar.settings.create', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createStore(Request $request, CreateCalendar $creator)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'name' => 'required|string|max:100',
|
||||||
|
'description' => 'nullable|string|max:255',
|
||||||
|
'timezone' => 'required|string',
|
||||||
|
'color' => 'nullable|regex:/^#[0-9A-Fa-f]{6}$/',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$creator->create($request->user(), $data);
|
||||||
|
$redirect = $data['redirect'] ?? route('calendar.index');
|
||||||
|
|
||||||
|
return redirect($redirect)->with('toast', [
|
||||||
|
'message' => __('Calendar created!'),
|
||||||
|
'type' => 'success',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe
|
* Subscribe
|
||||||
@ -133,7 +179,7 @@ class CalendarSettingsController extends Controller
|
|||||||
[
|
[
|
||||||
'source' => $data['source'],
|
'source' => $data['source'],
|
||||||
'displayname' => $data['displayname'] ?: $data['source'],
|
'displayname' => $data['displayname'] ?: $data['source'],
|
||||||
'calendarcolor' => $data['color'] ?? '#1a1a1a',
|
'calendarcolor' => calendar_color($data),
|
||||||
// you can add 'refreshrate' => 'P1D' here if you like
|
// you can add 'refreshrate' => 'P1D' here if you like
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@ -63,7 +63,7 @@ class SubscriptionController extends Controller
|
|||||||
'principaluri' => $principalUri,
|
'principaluri' => $principalUri,
|
||||||
'source' => $source,
|
'source' => $source,
|
||||||
'displayname' => $data['displayname'] ?: $source,
|
'displayname' => $data['displayname'] ?: $source,
|
||||||
'calendarcolor' => $data['color'] ?? '#1a1a1a',
|
'calendarcolor' => calendar_color($data),
|
||||||
'refreshrate' => 'P1D',
|
'refreshrate' => 'P1D',
|
||||||
'lastmodified' => now()->timestamp,
|
'lastmodified' => now()->timestamp,
|
||||||
]);
|
]);
|
||||||
@ -133,14 +133,13 @@ class SubscriptionController extends Controller
|
|||||||
$subscription->update($data);
|
$subscription->update($data);
|
||||||
|
|
||||||
// update corresponding calendar_meta record
|
// update corresponding calendar_meta record
|
||||||
|
$color = calendar_color(['color' => $subscription->calendarcolor]);
|
||||||
$subscription->meta()->updateOrCreate(
|
$subscription->meta()->updateOrCreate(
|
||||||
[], // no “where” clause → look at subscription_id FK
|
[], // no “where” clause → look at subscription_id FK
|
||||||
[
|
[
|
||||||
'title' => $subscription->displayname,
|
'title' => $subscription->displayname,
|
||||||
'color' => $subscription->calendarcolor ?? '#1a1a1a',
|
'color' => $color,
|
||||||
'color_fg' => contrast_text_color(
|
'color_fg' => contrast_text_color($color),
|
||||||
$subscription->calendarcolor ?? '#1a1a1a'
|
|
||||||
),
|
|
||||||
'updated_at'=> now(),
|
'updated_at'=> now(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@ -194,6 +194,9 @@ class SyncSubscription implements ShouldQueue
|
|||||||
'components' => 'VEVENT',
|
'components' => 'VEVENT',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// set the color
|
||||||
|
$color = calendar_color([], $meta->color);
|
||||||
|
|
||||||
// create the per-user instance (description is display-only; never used for lookup)
|
// create the per-user instance (description is display-only; never used for lookup)
|
||||||
CalendarInstance::create([
|
CalendarInstance::create([
|
||||||
'calendarid' => $calendar->id,
|
'calendarid' => $calendar->id,
|
||||||
@ -201,7 +204,7 @@ class SyncSubscription implements ShouldQueue
|
|||||||
'uri' => (string) Str::uuid(),
|
'uri' => (string) Str::uuid(),
|
||||||
'displayname' => $this->subscription->displayname,
|
'displayname' => $this->subscription->displayname,
|
||||||
'description' => $this->mirrorDescription($source),
|
'description' => $this->mirrorDescription($source),
|
||||||
'calendarcolor' => $meta->color ?? '#1a1a1a',
|
'calendarcolor' => $color,
|
||||||
'timezone' => config('app.timezone', 'UTC'),
|
'timezone' => config('app.timezone', 'UTC'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -69,10 +69,10 @@ class CalendarInstance extends Model
|
|||||||
public function resolvedColor(?string $fallback = null): string
|
public function resolvedColor(?string $fallback = null): string
|
||||||
{
|
{
|
||||||
// prefer meta color, fall back to sabre color, then default
|
// prefer meta color, fall back to sabre color, then default
|
||||||
return $this->meta?->color
|
return calendar_color(
|
||||||
?? $this->calendarcolor
|
['color' => $this->meta?->color ?? $this->calendarcolor],
|
||||||
?? $fallback
|
$fallback
|
||||||
?? '#1a1a1a';
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function resolvedColorFg(?string $fallback = null): string
|
public function resolvedColorFg(?string $fallback = null): string
|
||||||
|
|||||||
@ -46,6 +46,8 @@ class CalendarMeta extends Model
|
|||||||
*/
|
*/
|
||||||
public static function forSubscription(Subscription $sub): self
|
public static function forSubscription(Subscription $sub): self
|
||||||
{
|
{
|
||||||
|
$color = calendar_color([], $sub->calendarcolor);
|
||||||
|
|
||||||
return static::updateOrCreate(
|
return static::updateOrCreate(
|
||||||
// ---- unique match-key (subscription_id is unique, nullable) ----
|
// ---- unique match-key (subscription_id is unique, nullable) ----
|
||||||
['subscription_id' => $sub->id],
|
['subscription_id' => $sub->id],
|
||||||
@ -53,8 +55,8 @@ class CalendarMeta extends Model
|
|||||||
// ---- columns to fill / update ----
|
// ---- columns to fill / update ----
|
||||||
[
|
[
|
||||||
'title' => $sub->displayname,
|
'title' => $sub->displayname,
|
||||||
'color' => $sub->calendarcolor ?? '#1a1a1a',
|
'color' => $color,
|
||||||
'color_fg' => contrast_text_color($sub->calendarcolor ?? '#1a1a1a'),
|
'color_fg' => contrast_text_color($color),
|
||||||
'is_shared' => true,
|
'is_shared' => true,
|
||||||
'is_remote' => true,
|
'is_remote' => true,
|
||||||
// mirror_calendar_id is set later by the sync-job once the
|
// mirror_calendar_id is set later by the sync-job once the
|
||||||
|
|||||||
51
app/Services/Calendar/CreateCalendar.php
Normal file
51
app/Services/Calendar/CreateCalendar.php
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Calendar;
|
||||||
|
|
||||||
|
use App\Models\CalendarInstance;
|
||||||
|
use App\Models\CalendarMeta;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class CreateCalendar
|
||||||
|
{
|
||||||
|
public function create(User $user, array $data): CalendarInstance
|
||||||
|
{
|
||||||
|
$color = calendar_color($data);
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($user, $data, $color) {
|
||||||
|
|
||||||
|
/* create master calendar container (sabre table) */
|
||||||
|
$calId = DB::table('calendars')->insertGetId([
|
||||||
|
'synctoken' => 1,
|
||||||
|
'components' => 'VEVENT',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/* create per-user instance (sabre table) */
|
||||||
|
$instance = CalendarInstance::create([
|
||||||
|
'calendarid' => $calId,
|
||||||
|
'principaluri' => $user->uri, // your principal uri
|
||||||
|
'uri' => (string) Str::uuid(), // instance slug
|
||||||
|
'displayname' => $data['name'],
|
||||||
|
'description' => $data['description'] ?? null,
|
||||||
|
'calendarcolor' => $color, // keep sabre in sync
|
||||||
|
'timezone' => $data['timezone'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
/* create ui meta */
|
||||||
|
CalendarMeta::updateOrCreate(
|
||||||
|
['calendar_id' => $calId], // calendar_id = sabre calendars.id (int)
|
||||||
|
[
|
||||||
|
'title' => $data['name'],
|
||||||
|
'color' => $color,
|
||||||
|
'color_fg' => contrast_text_color($color),
|
||||||
|
'is_remote' => false,
|
||||||
|
'is_shared' => false,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $instance;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -65,3 +65,89 @@ if (! function_exists('contrast_text_color')) {
|
|||||||
return $useDark ? $dark : $light;
|
return $useDark ? $dark : $light;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! function_exists('is_hex_color')) {
|
||||||
|
function is_hex_color(mixed $value): bool
|
||||||
|
{
|
||||||
|
return is_string($value) && preg_match('/^#[0-9A-Fa-f]{6}$/', $value) === 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! function_exists('calendar_color_palette')) {
|
||||||
|
/**
|
||||||
|
* returns a cleaned palette from config (valid hex only).
|
||||||
|
*
|
||||||
|
* @return array<int,string>
|
||||||
|
*/
|
||||||
|
function calendar_color_palette(): array
|
||||||
|
{
|
||||||
|
$palette = config('kithkin.calendar.color_palette', []);
|
||||||
|
|
||||||
|
if (! is_array($palette)) {
|
||||||
|
$palette = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_filter($palette, fn ($c) => is_hex_color($c)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! function_exists('calendar_color_failsafe')) {
|
||||||
|
function calendar_color_failsafe(): string
|
||||||
|
{
|
||||||
|
$fallback = config('kithkin.calendar.color_failsafe', '#1a1a1a');
|
||||||
|
|
||||||
|
return is_hex_color($fallback) ? $fallback : '#1a1a1a';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! function_exists('default_calendar_color')) {
|
||||||
|
/**
|
||||||
|
* returns the default calendar color from the configured palette.
|
||||||
|
* if the palette is empty/invalid, falls back to the failsafe.
|
||||||
|
*/
|
||||||
|
function default_calendar_color(): string
|
||||||
|
{
|
||||||
|
$palette = calendar_color_palette();
|
||||||
|
|
||||||
|
if (count($palette) === 0) {
|
||||||
|
return calendar_color_failsafe();
|
||||||
|
}
|
||||||
|
|
||||||
|
$strategy = config('kithkin.calendar.default_color_strategy', 'first');
|
||||||
|
|
||||||
|
if ($strategy === 'random') {
|
||||||
|
return $palette[array_rand($palette)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $palette[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! function_exists('calendar_color')) {
|
||||||
|
/**
|
||||||
|
* pick a calendar color with a single source of truth for defaults.
|
||||||
|
*
|
||||||
|
* order:
|
||||||
|
* 1) $data['color'] if valid
|
||||||
|
* 2) $fallback if valid
|
||||||
|
* 3) default_calendar_color() (palette-based)
|
||||||
|
* 4) hard failsafe from config, then '#1a1a1a'
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $data
|
||||||
|
*/
|
||||||
|
function calendar_color(array $data = [], ?string $fallback = null): string
|
||||||
|
{
|
||||||
|
$raw = $data['color'] ?? null;
|
||||||
|
|
||||||
|
if (is_hex_color($raw)) {
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_hex_color($fallback)) {
|
||||||
|
return $fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
$default = default_calendar_color();
|
||||||
|
return is_hex_color($default) ? $default : calendar_color_failsafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
19
config/kithkin.php
Normal file
19
config/kithkin.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'calendar' => [
|
||||||
|
// used by the color picker and as the default pool for calendars
|
||||||
|
'color_palette' => [
|
||||||
|
'#2563eb', '#7c3aed', '#db2777', '#ef4444', '#f97316',
|
||||||
|
'#f59e0b', '#84cc16', '#22c55e', '#14b8a6', '#06b6d4',
|
||||||
|
'#0ea5e9', '#64748b', '#d051cc', '#ffec05', '#739399',
|
||||||
|
],
|
||||||
|
|
||||||
|
// absolute failsafe if everything else is missing or invalid
|
||||||
|
'color_failsafe' => '#1a1a1a',
|
||||||
|
|
||||||
|
// how default_calendar_color() chooses from the palette
|
||||||
|
// options: 'first' | 'random'
|
||||||
|
'default_color_strategy' => 'random',
|
||||||
|
],
|
||||||
|
];
|
||||||
@ -13,17 +13,23 @@ return [
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
'color' => 'Color',
|
'color' => 'Color',
|
||||||
|
'create' => 'Create calendar',
|
||||||
'description' => 'Description',
|
'description' => 'Description',
|
||||||
'ics' => [
|
'ics' => [
|
||||||
'url' => 'ICS URL',
|
'url' => 'ICS URL',
|
||||||
'url_help' => 'You can\'t edit a public calendar URL. If you need to make a change, unsubscribe and add it again.',
|
'url_help' => 'You can\'t edit a public calendar URL. If you need to make a change, unsubscribe and add it again.',
|
||||||
],
|
],
|
||||||
|
'mine' => 'My calendars',
|
||||||
'name' => 'Calendar name',
|
'name' => 'Calendar name',
|
||||||
'settings' => [
|
'settings' => [
|
||||||
'calendar' => [
|
'calendar' => [
|
||||||
'title' => 'Calendar settings',
|
'title' => 'Calendar settings',
|
||||||
'subtitle' => 'Details and settings for <strong>:calendar</strong>.'
|
'subtitle' => 'Details and settings for <strong>:calendar</strong>.'
|
||||||
],
|
],
|
||||||
|
'create' => [
|
||||||
|
'title' => 'Create a calendar',
|
||||||
|
'subtitle' => 'Create a new local calendar.',
|
||||||
|
],
|
||||||
'language_region' => [
|
'language_region' => [
|
||||||
'title' => 'Language and region',
|
'title' => 'Language and region',
|
||||||
'subtitle' => 'Choose your default language, region, and formatting preferences. These affect how dates and times are displayed in your calendars and events.',
|
'subtitle' => 'Choose your default language, region, and formatting preferences. These affect how dates and times are displayed in your calendars and events.',
|
||||||
|
|||||||
@ -16,6 +16,7 @@ return [
|
|||||||
'calendar' => 'Calendar',
|
'calendar' => 'Calendar',
|
||||||
'calendars' => 'Calendars',
|
'calendars' => 'Calendars',
|
||||||
'cancel' => 'Cancel',
|
'cancel' => 'Cancel',
|
||||||
|
'cancel_back' => 'Cancel and go back',
|
||||||
'cancel_funny' => 'Get me out of here',
|
'cancel_funny' => 'Get me out of here',
|
||||||
'date' => 'Date',
|
'date' => 'Date',
|
||||||
'date_select' => 'Select a date',
|
'date_select' => 'Select a date',
|
||||||
|
|||||||
@ -105,7 +105,7 @@ main {
|
|||||||
|
|
||||||
/* if there's an aside, set the cols */
|
/* if there's an aside, set the cols */
|
||||||
&:has(aside) {
|
&:has(aside) {
|
||||||
grid-template-columns: minmax(20rem, 20dvw) auto;
|
grid-template-rows: 4rem 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,13 +118,10 @@ main {
|
|||||||
|
|
||||||
/* left column */
|
/* left column */
|
||||||
aside {
|
aside {
|
||||||
@apply bg-white flex flex-col col-span-1 pb-8 h-full rounded-l-lg;
|
@apply flex flex-col col-span-1 pb-8 h-16 overflow-hidden rounded-l-lg;
|
||||||
@apply overflow-y-auto;
|
|
||||||
|
|
||||||
> h1 {
|
> h1 {
|
||||||
@apply flex items-center h-20 min-h-20 px-6 2xl:px-8;
|
@apply flex items-center h-16 min-h-16 px-6 2xl:px-8;
|
||||||
@apply backdrop-blur-xs sticky top-0 z-1 shrink-0;
|
|
||||||
background-color: rgba(255, 255, 255, 0.9);
|
|
||||||
|
|
||||||
a.app-return {
|
a.app-return {
|
||||||
@apply flex flex-row gap-2 items-center;
|
@apply flex flex-row gap-2 items-center;
|
||||||
@ -185,7 +182,7 @@ main {
|
|||||||
|
|
||||||
/* main content wrapper */
|
/* main content wrapper */
|
||||||
article {
|
article {
|
||||||
@apply bg-white grid grid-cols-1 w-full pl-3 2xl:pl-4 pr-6 2xl:pr-8 rounded-r-lg;
|
@apply bg-white grid grid-cols-1 ml-2 rounded-md;
|
||||||
@apply overflow-y-auto;
|
@apply overflow-y-auto;
|
||||||
grid-template-rows: 5rem auto;
|
grid-template-rows: 5rem auto;
|
||||||
|
|
||||||
@ -309,6 +306,26 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
&:has(aside) {
|
||||||
|
grid-template-columns: minmax(20rem, 20dvw) auto;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside {
|
||||||
|
@apply bg-white overflow-y-auto h-full;
|
||||||
|
|
||||||
|
> h1 {
|
||||||
|
@apply backdrop-blur-xs sticky top-0 z-1 shrink-0 h-20 min-h-20;
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
article {
|
||||||
|
@apply w-full ml-0 pl-3 2xl:pl-4 pr-6 2xl:pr-8 rounded-l-none rounded-r-lg;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,12 +26,12 @@
|
|||||||
|
|
||||||
/* app name */
|
/* app name */
|
||||||
h1 {
|
h1 {
|
||||||
@apply font-serif text-3xl font-extrabold leading-tight;
|
@apply font-serif text-2xl md:text-3xl font-extrabold leading-tight;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* page header */
|
/* page header */
|
||||||
h2 {
|
h2 {
|
||||||
@apply font-serif text-2xl font-extrabold leading-tight text-primary;
|
@apply font-serif text-xl md:text-2xl font-extrabold leading-tight text-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* section dividers */
|
/* section dividers */
|
||||||
|
|||||||
@ -2,12 +2,14 @@ button,
|
|||||||
.button {
|
.button {
|
||||||
@apply relative inline-flex items-center cursor-pointer gap-2 rounded-md h-11 px-4 text-lg font-medium;
|
@apply relative inline-flex items-center cursor-pointer gap-2 rounded-md h-11 px-4 text-lg font-medium;
|
||||||
/*transition: background-color 125ms ease-in-out; */
|
/*transition: background-color 125ms ease-in-out; */
|
||||||
@apply focus:outline-md focus:outline-offset-2 focus:outline-secondary;
|
@apply focus:ring-2 focus:ring-offset-2 focus:ring-cyan-600 focus:outline-none;
|
||||||
|
transition: border-color 125ms ease-in-out;
|
||||||
--button-border: var(--color-primary);
|
--button-border: var(--color-primary);
|
||||||
--button-accent: var(--color-primary-hover);
|
--button-accent: var(--color-primary-hover);
|
||||||
|
|
||||||
|
|
||||||
&.button--primary {
|
&.button--primary {
|
||||||
@apply bg-cyan-400 border-md border-solid;
|
@apply bg-cyan-400 border-md border-solid focus:border-primary;
|
||||||
border-color: var(--button-border);
|
border-color: var(--button-border);
|
||||||
box-shadow: 2.5px 2.5px 0 0 var(--button-border);
|
box-shadow: 2.5px 2.5px 0 0 var(--button-border);
|
||||||
|
|
||||||
@ -17,7 +19,10 @@ button,
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
@apply shadow-none;
|
box-shadow:
|
||||||
|
2.5px 2.5px 0 0 var(--button-border),
|
||||||
|
0 0 0 2px var(--color-white),
|
||||||
|
0 0 0 4px var(--color-cyan-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
|
|||||||
@ -74,6 +74,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* calendar list in the left bar */
|
/* calendar list in the left bar */
|
||||||
|
#calendar-toggles {
|
||||||
|
|
||||||
|
summary {
|
||||||
|
@apply flex items-center gap-1 justify-start;
|
||||||
|
|
||||||
|
span {
|
||||||
|
@apply capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
@apply hidden -mt-2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
a {
|
||||||
|
@apply flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
li.calendar-toggle {
|
li.calendar-toggle {
|
||||||
@apply relative;
|
@apply relative;
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,27 @@
|
|||||||
.colorpicker {
|
.colorpicker {
|
||||||
@apply inline-flex items-center rounded-md h-11;
|
@apply inline-flex items-center rounded-md h-11 shadow-input;
|
||||||
@apply border-md border-secondary shadow-input;
|
|
||||||
|
|
||||||
input[type="color"] {
|
input[type="color"] {
|
||||||
@apply rounded-l-md-inset rounded-r-none h-full shrink-0 min-w-11;
|
@apply rounded-l-md-inset rounded-r-none h-full shrink-0 min-w-11;
|
||||||
|
@apply border-md border-secondary border-r-0 outline-transparent;
|
||||||
|
transition: outline 125ms ease-in-out;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
@apply border-primary outline-2 outline-offset-2 outline-cyan-600 z-2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"] {
|
input[type="text"] {
|
||||||
@apply rounded-none grow min-w-12;
|
@apply rounded-none grow min-w-12 font-mono;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@apply w-11 min-w-11 h-full shrink-0 flex items-center justify-center rounded-l-none rounded-r-md;
|
@apply w-11 min-w-11 h-11 min-h-11 shrink-0 flex items-center justify-center relative right-0;
|
||||||
|
@apply border-md border-secondary rounded-l-none rounded-r-md -ml-[1.5px];
|
||||||
|
@apply focus:border-primary;
|
||||||
|
transition: background-color 125ms ease-in-out,
|
||||||
|
box-shadow 125ms ease-in-out,
|
||||||
|
border-color 125ms ease-in-out;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply bg-teal-100 shadow-input-hover;
|
@apply bg-teal-100 shadow-input-hover;
|
||||||
@ -22,23 +32,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.colorpicker__swatch {
|
|
||||||
width: 2.5rem;
|
|
||||||
height: 2.25rem;
|
|
||||||
padding: 0;
|
|
||||||
border: 1px solid var(--border, #d1d5db);
|
|
||||||
border-radius: .5rem;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colorpicker__hex {
|
|
||||||
@apply w-32 font-mono;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colorpicker__random:disabled,
|
|
||||||
.colorpicker__swatch:disabled,
|
|
||||||
.colorpicker__hex:disabled {
|
|
||||||
opacity: .6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -55,6 +55,13 @@ input[type="radio"] {
|
|||||||
@apply flex flex-row gap-2 items-center;
|
@apply flex flex-row gap-2 items-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* textareas
|
||||||
|
*/
|
||||||
|
textarea {
|
||||||
|
@apply min-h-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* specific minor types
|
* specific minor types
|
||||||
@ -89,6 +96,14 @@ form {
|
|||||||
&.settings {
|
&.settings {
|
||||||
@apply mt-8;
|
@apply mt-8;
|
||||||
@apply 2xl:max-w-3xl;
|
@apply 2xl:max-w-3xl;
|
||||||
|
|
||||||
|
&.modal {
|
||||||
|
@apply mt-0;
|
||||||
|
|
||||||
|
.input-row {
|
||||||
|
@apply !mt-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
article.settings {
|
article.settings {
|
||||||
|
|||||||
1
resources/svg/icons/calendar-plus.svg
Normal file
1
resources/svg/icons/calendar-plus.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 19h6"/><path d="M16 2v4"/><path d="M19 16v6"/><path d="M21 12.598V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8.5"/><path d="M3 10h18"/><path d="M8 2v4"/></svg>
|
||||||
|
After Width: | Height: | Size: 359 B |
@ -1 +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>
|
<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"><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>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 408 B After Width: | Height: | Size: 363 B |
1
resources/svg/icons/solid/calendar-plus.svg
Normal file
1
resources/svg/icons/solid/calendar-plus.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><path d="M12.534,22.943c-0.061,0.004 -7.534,0.057 -7.534,0.057c-1.646,0 -3,-1.354 -3,-3l0,-14c0,-1.646 1.354,-3 3,-3l2,0l0,-1c-0,-0.552 0.448,-1 1,-1c0.552,-0 1,0.448 1,1l0,1l6,0l0,-1c-0,-0.552 0.448,-1 1,-1c0.552,-0 1,0.448 1,1l0,1l2,0c1.646,0 3,1.354 3,3l0,5.738c0.018,0.068 0.03,0.139 0.033,0.212c0.027,0.551 -0.398,1.021 -0.949,1.048c-5.045,0.25 -7.682,3.879 -7.631,8.796c0.006,0.552 -0.369,1.111 -0.92,1.148Zm-5.534,-17.943l-2,0c-0.549,0 -1,0.451 -1,1l0,3l16,0l0,-3c0,-0.549 -0.451,-1 -1,-1l-2,0l0,1c-0,0.552 -0.448,1 -1,1c-0.552,-0 -1,-0.448 -1,-1l0,-1l-6,0l-0,1c-0,0.552 -0.448,1 -1,1c-0.552,-0 -1,-0.448 -1,-1l0,-1Zm11,15l-2,0c-0.552,0 -1,-0.448 -1,-1c-0,-0.552 0.448,-1 1,-1l2,0l0,-2c0,-0.552 0.448,-1 1,-1c0.552,0 1,0.448 1,1l0,2l2,0c0.552,0 1,0.448 1,1c-0,0.552 -0.448,1 -1,1l-2,0l0,2c0,0.552 -0.448,1 -1,1c-0.552,0 -1,-0.448 -1,-1l0,-2Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
resources/svg/icons/solid/home.svg
Normal file
1
resources/svg/icons/solid/home.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M2,10c-0,-0.883 0.389,-1.722 1.058,-2.287l7,-5.999l0.005,-0.004c1.113,-0.941 2.76,-0.941 3.873,-0l0.005,0.004l6.995,5.995c0.674,0.57 1.064,1.409 1.064,2.292l0,9c0,1.646 -1.354,3 -3,3l-14,0c-1.646,0 -3,-1.354 -3,-3l0,-9Zm13.5,9.517l0,-6.517c0,-0.823 -0.677,-1.5 -1.5,-1.5l-4,0c-0.823,0 -1.5,0.677 -1.5,1.5l0,6.517l7,0Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 452 B |
@ -10,7 +10,15 @@
|
|||||||
action="{{ route('calendar.index') }}"
|
action="{{ route('calendar.index') }}"
|
||||||
method="get">
|
method="get">
|
||||||
<details open>
|
<details open>
|
||||||
<summary>{{ __('My Calendars') }}</summary>
|
<summary>
|
||||||
|
<span>{{ __('calendar.mine') }}</span>
|
||||||
|
<a href="{{ route('calendar.settings.create') }}"
|
||||||
|
hx-get="{{ route('calendar.settings.create') }}"
|
||||||
|
hx-target="#modal"
|
||||||
|
hx-push-url="false"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="button button--icon button--sm">+</a>
|
||||||
|
</summary>
|
||||||
<ul class="content">
|
<ul class="content">
|
||||||
@foreach ($calendars->where('is_remote', false) as $cal)
|
@foreach ($calendars->where('is_remote', false) as $cal)
|
||||||
<li class="calendar-toggle">
|
<li class="calendar-toggle">
|
||||||
|
|||||||
51
resources/views/calendar/partials/create-modal.blade.php
Normal file
51
resources/views/calendar/partials/create-modal.blade.php
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<x-modal.content>
|
||||||
|
<x-modal.title>
|
||||||
|
{{ __('calendar.settings.create.title') }}
|
||||||
|
</x-modal.title>
|
||||||
|
<x-modal.body>
|
||||||
|
<form id="create-calendar-form" method="post" action="{{ route('calendar.store') }}" class="settings modal">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<input type="hidden" name="redirect" value="{{ $redirect ?? route('calendar.index') }}">
|
||||||
|
|
||||||
|
<div class="input-row input-row--1">
|
||||||
|
<div class="input-cell">
|
||||||
|
<x-input.label for="name" :value="__('Name')" />
|
||||||
|
<x-input.text id="name" name="name" type="text" :value="old('name', $defaults['name'] ?? '')" required />
|
||||||
|
<x-input.error :messages="$errors->get('name')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-row input-row--1">
|
||||||
|
<div class="input-cell">
|
||||||
|
<x-input.label for="description" :value="__('Description')" />
|
||||||
|
<x-input.textarea id="description" name="description" type="text" :value="old('description', $defaults['description'] ?? '')" />
|
||||||
|
<x-input.error :messages="$errors->get('description')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-row input-row--1-1">
|
||||||
|
<div class="input-cell">
|
||||||
|
<x-input.label for="timezone" :value="__('Timezone')" />
|
||||||
|
<x-input.select id="timezone" name="timezone" :options="config('timezones')" :selected="old('timezone', $defaults['timezone'] ?? 'UTC')" />
|
||||||
|
<x-input.error :messages="$errors->get('timezone')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-cell">
|
||||||
|
<x-input.label for="color" :value="__('Color')" />
|
||||||
|
<x-input.color-picker id="color" name="color" :value="old('color', $defaults['color'] ?? '#1a1a1a')" />
|
||||||
|
<x-input.error :messages="$errors->get('color')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</x-modal.body>
|
||||||
|
<x-modal.footer>
|
||||||
|
<x-button variant="secondary" onclick="this.closest('dialog')?.close()">
|
||||||
|
{{ __('common.cancel') }}
|
||||||
|
</x-button>
|
||||||
|
<x-button variant="primary" type="submit" form="create-calendar-form">
|
||||||
|
{{ __('calendar.create') }}
|
||||||
|
</x-button>
|
||||||
|
</x-modal.footer>
|
||||||
|
</x-modal.content>
|
||||||
51
resources/views/calendar/settings/create.blade.php
Normal file
51
resources/views/calendar/settings/create.blade.php
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<div class="description">
|
||||||
|
<p>
|
||||||
|
{{ $data['sub'] }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="{{ route('calendar.store') }}" class="settings">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<input type="hidden" name="redirect" value="{{ $redirect ?? route('calendar.index') }}">
|
||||||
|
|
||||||
|
<div class="input-row input-row--1">
|
||||||
|
<div class="input-cell">
|
||||||
|
<x-input.label for="name" :value="__('Name')" />
|
||||||
|
<x-input.text id="name" name="name" type="text" :value="old('name', $defaults['name'] ?? '')" required />
|
||||||
|
<x-input.error :messages="$errors->get('name')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-row input-row--1">
|
||||||
|
<div class="input-cell">
|
||||||
|
<x-input.label for="description" :value="__('Description')" />
|
||||||
|
<x-input.textarea id="description" name="description" type="text" :value="old('description', $defaults['description'] ?? '')" />
|
||||||
|
<x-input.error :messages="$errors->get('description')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-row input-row--1-1">
|
||||||
|
<div class="input-cell">
|
||||||
|
<x-input.label for="timezone" :value="__('common.timezone')" />
|
||||||
|
<x-input.select
|
||||||
|
id="timezone"
|
||||||
|
name="timezone"
|
||||||
|
placeholder="{{ __('common.timezone_select') }}"
|
||||||
|
:options="$timezones"
|
||||||
|
:selected="old('timezone', $user->timezone ?? '')"
|
||||||
|
:description="__('calendar.timezone_help')" />
|
||||||
|
<x-input.error :messages="$errors->get('timezone')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-cell">
|
||||||
|
<x-input.label for="color" :value="__('Color')" />
|
||||||
|
<x-input.color-picker id="color" name="color" :value="old('color', $defaults['color'] ?? default_calendar_color())" />
|
||||||
|
<x-input.error :messages="$errors->get('color')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-row input-row--actions input-row--start sticky-bottom">
|
||||||
|
<x-button variant="primary" type="submit">{{ __('calendar.create') }}</x-button>
|
||||||
|
<x-button type="anchor" variant="tertiary" href="{{ route('calendar.index') }}">{{ __('common.cancel_back') }}</x-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@ -1,25 +1,28 @@
|
|||||||
@props([
|
@props([
|
||||||
'id' => null,
|
'id' => null,
|
||||||
'name' => 'color',
|
'name' => 'color',
|
||||||
'value' => null, // initial hex, e.g. "#0038ff"
|
'value' => null,
|
||||||
'palette' => null, // optional override array of hex strings
|
'palette' => null,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'disabled' => false,
|
'disabled' => false,
|
||||||
])
|
])
|
||||||
|
|
||||||
@php
|
@php
|
||||||
$id = $id ?: 'color_'.Str::uuid();
|
$id = $id ?: 'color_'.Str::uuid();
|
||||||
$initial = old($name, $value) ?: '#1a1a1a';
|
|
||||||
|
|
||||||
// palette of pre-selected colors to cycle through
|
$paletteColors = is_array($palette) && count($palette)
|
||||||
$pleasant = $palette ?: [
|
? array_values(array_filter($palette, fn ($c) => is_hex_color($c)))
|
||||||
'#2563eb', '#7c3aed', '#db2777', '#ef4444', '#f97316',
|
: calendar_color_palette();
|
||||||
'#f59e0b', '#84cc16', '#22c55e', '#14b8a6', '#06b6d4',
|
|
||||||
'#0ea5e9', '#64748b', '#d051cc', '#ffec05', '#739399',
|
// initial: old() -> explicit value -> palette-based default -> failsafe
|
||||||
];
|
$initial = old($name, $value) ?: default_calendar_color();
|
||||||
|
|
||||||
|
if (! is_hex_color($initial)) {
|
||||||
|
$initial = calendar_color_failsafe();
|
||||||
|
}
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="colorpicker" data-colorpicker data-palette='@json(array_values($pleasant))'>
|
<div class="colorpicker" data-colorpicker data-palette='@json($paletteColors)'>
|
||||||
<input
|
<input
|
||||||
id="{{ $id }}"
|
id="{{ $id }}"
|
||||||
type="color"
|
type="color"
|
||||||
@ -50,5 +53,4 @@
|
|||||||
>
|
>
|
||||||
<x-icon-d20 width="20" />
|
<x-icon-d20 width="20" />
|
||||||
</x-button>
|
</x-button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -15,6 +15,14 @@
|
|||||||
<details open>
|
<details open>
|
||||||
<summary>{{ __('Add a calendar') }}</summary>
|
<summary>{{ __('Add a calendar') }}</summary>
|
||||||
<menu class="content pagelinks">
|
<menu class="content pagelinks">
|
||||||
|
<li>
|
||||||
|
<x-app.pagelink
|
||||||
|
href="{{ route('calendar.settings.create') }}"
|
||||||
|
:active="request()->routeIs('calendar.settings.create')"
|
||||||
|
:label="__('calendar.settings.create.title')"
|
||||||
|
icon="calendar-plus"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<x-app.pagelink
|
<x-app.pagelink
|
||||||
href="{{ route('calendar.settings.subscribe') }}"
|
href="{{ route('calendar.settings.subscribe') }}"
|
||||||
|
|||||||
@ -89,13 +89,17 @@ Route::middleware('auth')->group(function ()
|
|||||||
Route::get('settings/subscribe', [CalendarSettingsController::class, 'subscribeForm'])->name('settings.subscribe');
|
Route::get('settings/subscribe', [CalendarSettingsController::class, 'subscribeForm'])->name('settings.subscribe');
|
||||||
Route::post('settings/subscribe', [CalendarSettingsController::class, 'subscribeStore'])->name('settings.subscribe.store');
|
Route::post('settings/subscribe', [CalendarSettingsController::class, 'subscribeStore'])->name('settings.subscribe.store');
|
||||||
|
|
||||||
|
// settings / create a calendar
|
||||||
|
Route::get('settings/create', [CalendarSettingsController::class, 'createForm'])->name('settings.create');
|
||||||
|
Route::post('settings/create', [CalendarSettingsController::class, 'createStore'])->name('settings.create.store');
|
||||||
|
|
||||||
// remote calendar subscriptions
|
// remote calendar subscriptions
|
||||||
Route::resource('subscriptions', SubscriptionController::class)
|
Route::resource('subscriptions', SubscriptionController::class)
|
||||||
->except(['show']); // index, create, store, edit, update, destroy
|
->except(['show']); // index, create, store, edit, update, destroy
|
||||||
|
|
||||||
// calendar settings for a specific calendar instance/container
|
// calendar settings for a specific calendar instance/container
|
||||||
Route::get('settings/calendars/{calendarUri}', [CalendarSettingsController::class, 'calendarForm'])
|
Route::get('settings/calendars/{calendarUri}', [CalendarSettingsController::class, 'calendarForm'])
|
||||||
->whereUuid('calendarUri') // sabre calendarid is an int
|
->whereUuid('calendarUri')
|
||||||
->name('settings.calendars.show');
|
->name('settings.calendars.show');
|
||||||
Route::patch('settings/calendars/{calendarUri}', [CalendarSettingsController::class, 'calendarStore'])
|
Route::patch('settings/calendars/{calendarUri}', [CalendarSettingsController::class, 'calendarStore'])
|
||||||
->whereUuid('calendarUri')
|
->whereUuid('calendarUri')
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user