Adds new green color palette, removes any requirement of a remote calendar description as validation, fixes language settings pane

This commit is contained in:
Andrew Gioia 2026-01-23 14:49:52 -05:00
parent 1a92f09e3b
commit 6242a9772a
Signed by: andrew
GPG Key ID: FC09694A000800C8
11 changed files with 153 additions and 121 deletions

View File

@ -28,6 +28,7 @@ class SubscriptionController extends Controller
public function store(Request $request) public function store(Request $request)
{ {
/* validate the submission */
$data = $request->validate([ $data = $request->validate([
'source' => 'required|url', 'source' => 'required|url',
'displayname' => 'nullable|string|max:255', 'displayname' => 'nullable|string|max:255',
@ -35,25 +36,26 @@ class SubscriptionController extends Controller
'refreshrate' => 'nullable|string|max:10', 'refreshrate' => 'nullable|string|max:10',
]); ]);
$principalUri = $request->user()->uri; /* normalize hard to check against this url */
$source = rtrim(trim($data['source']), '/'); $source = rtrim(trim($data['source']), '/');
$desc = 'Remote feed: '.$source;
/* if they already subscribed to this exact feed, dont create duplicates */ /* set the principal URI */
if (Subscription::where('principaluri', $principalUri)->where('source', $source)->exists()) { $principalUri = $request->user()->uri;
/* check for existing subscriptions */
$already = Subscription::query()
->where('principaluri', $principalUri)
->where('source', $source)
->exists();
if ($already) {
return Redirect::route('calendar.index')->with('toast', [ return Redirect::route('calendar.index')->with('toast', [
'message' => __('You are already subscribed to that calendar.'), 'message' => __('You are already subscribed to that calendar.'),
'type' => 'info', 'type' => 'info',
]); ]);
} }
$sub = DB::transaction(function () use ($request, $data, $principalUri, $source, $desc) { $sub = DB::transaction(function () use ($data, $principalUri, $source) {
/* check if a mirror instance already exists */
$existingInstance = CalendarInstance::where('principaluri', $principalUri)
->where('description', $desc)
->first();
/* create the calendarsubscriptions record */ /* create the calendarsubscriptions record */
$sub = Subscription::create([ $sub = Subscription::create([
@ -66,36 +68,24 @@ class SubscriptionController extends Controller
'lastmodified' => now()->timestamp, 'lastmodified' => now()->timestamp,
]); ]);
// choose the calendar container // create new empty calendar container
if ($existingInstance) { $calId = Calendar::create([
$calId = $existingInstance->calendarid; 'synctoken' => 1,
'components' => 'VEVENT',
])->id;
// keep the mirror instances user-facing bits up to date // create mirror calendarinstance row (description can be user-editable, not relied on)
$existingInstance->update([ CalendarInstance::create([
'displayname' => $sub->displayname, 'calendarid' => $calId,
'calendarcolor' => $sub->calendarcolor, 'principaluri' => $principalUri,
'timezone' => config('app.timezone', 'UTC'), 'uri' => (string) Str::uuid(),
]); 'displayname' => $sub->displayname,
} else { 'description' => 'Remote feed: '.$source, // informational only
// create new empty calendar container 'calendarcolor' => $sub->calendarcolor,
$calId = Calendar::create([ 'timezone' => config('app.timezone', 'UTC'),
'synctoken' => 1, ]);
'components' => 'VEVENT',
])->id;
// create mirror calendarinstance row // meta entry
CalendarInstance::create([
'calendarid' => $calId,
'principaluri' => $sub->principaluri,
'uri' => Str::uuid(),
'displayname' => $sub->displayname,
'description' => $desc,
'calendarcolor' => $sub->calendarcolor,
'timezone' => config('app.timezone', 'UTC'),
]);
}
// upsert our calendar_meta entry by calendar_id (since thats your pk)
CalendarMeta::updateOrCreate( CalendarMeta::updateOrCreate(
['calendar_id' => $calId], ['calendar_id' => $calId],
[ [
@ -114,12 +104,10 @@ class SubscriptionController extends Controller
// sync immediately so events appear without waiting for the */10 dispatcher // sync immediately so events appear without waiting for the */10 dispatcher
SyncSubscription::dispatch($sub)->afterCommit(); SyncSubscription::dispatch($sub)->afterCommit();
return redirect() return Redirect::route('calendar.index')->with('toast', [
->route('calendar.index') 'message' => __('Subscription added! Syncing events now...'),
->with('toast', [ 'type' => 'success',
'message' => __('Subscription added! Syncing events now...'), ]);
'type' => 'success',
]);
} }
public function edit(Subscription $subscription) public function edit(Subscription $subscription)

View File

@ -188,27 +188,19 @@ class SyncSubscription implements ShouldQueue
return (int) $meta->calendar_id; return (int) $meta->calendar_id;
} }
$desc = $this->mirrorDescription($source); // create a new master calendar in `calendars`
$existing = CalendarInstance::where('principaluri', $this->subscription->principaluri)
->where('description', $desc)
->first();
if ($existing) {
return (int) $existing->calendarid;
}
$calendar = Calendar::create([ $calendar = Calendar::create([
'synctoken' => 1, 'synctoken' => 1,
'components' => 'VEVENT', 'components' => 'VEVENT',
]); ]);
// create the per-user instance (description is display-only; never used for lookup)
CalendarInstance::create([ CalendarInstance::create([
'calendarid' => $calendar->id, 'calendarid' => $calendar->id,
'principaluri' => $this->subscription->principaluri, 'principaluri' => $this->subscription->principaluri,
'uri' => (string) Str::uuid(), 'uri' => (string) Str::uuid(),
'displayname' => $this->subscription->displayname, 'displayname' => $this->subscription->displayname,
'description' => $desc, 'description' => $this->mirrorDescription($source),
'calendarcolor' => $meta->color ?? '#1a1a1a', 'calendarcolor' => $meta->color ?? '#1a1a1a',
'timezone' => config('app.timezone', 'UTC'), 'timezone' => config('app.timezone', 'UTC'),
]); ]);

View File

@ -26,7 +26,7 @@ return [
], ],
'language_region' => [ 'language_region' => [
'title' => 'Language and region', 'title' => 'Language and region',
'subtitle' => 'Choose your default language, region, and formatting preferences for calendars. These affect how dates and times are displayed throughout Kithkin.', 'subtitle' => 'Choose your default language, region, and formatting preferences. These affect how dates and times are displayed in your calendars and events.',
], ],
'my_calendars' => 'Settings for my calendars', 'my_calendars' => 'Settings for my calendars',
'subscribe' => [ 'subscribe' => [

View File

@ -17,11 +17,23 @@ return [
'calendars' => 'Calendars', 'calendars' => 'Calendars',
'cancel' => 'Cancel', 'cancel' => 'Cancel',
'cancel_funny' => 'Get me out of here', 'cancel_funny' => 'Get me out of here',
'date' => 'Date',
'date_select' => 'Select a date',
'date_format' => 'Date format',
'date_format_select' => 'Select a date format',
'event' => 'Event', 'event' => 'Event',
'events' => 'Events', 'events' => 'Events',
'language' => 'Language',
'language_select' => 'Select a language',
'password' => 'Password', 'password' => 'Password',
'region' => 'Region',
'region_select' => 'Select a region',
'save_changes' => 'Save changes', 'save_changes' => 'Save changes',
'settings' => 'Settings', 'settings' => 'Settings',
'time' => 'Time',
'time_select' => 'Select a time',
'time_format' => 'Time format',
'time_format_select' => 'Select a time format',
'timezone' => 'Time zone', 'timezone' => 'Time zone',
'timezone_select' => 'Select a time zone', 'timezone_select' => 'Select a time zone',

View File

@ -125,6 +125,23 @@ main {
@apply flex items-center h-20 min-h-20 px-6 2xl:px-8; @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; @apply backdrop-blur-xs sticky top-0 z-1 shrink-0;
background-color: rgba(255, 255, 255, 0.9); background-color: rgba(255, 255, 255, 0.9);
a.app-return {
@apply flex flex-row gap-2 items-center;
> svg {
@apply opacity-0 invisible -translate-x-1 text-white mt-1;
transition:
color 150ms ease-in,
opacity 150ms ease-in,
visibility 150ms ease-in,
translate 150ms ease-in;
}
&:hover > svg {
@apply opacity-100 visible translate-x-0 text-secondary;
}
}
} }
> .aside-inset { > .aside-inset {
@ -248,11 +265,11 @@ main {
@keyframes title-drop { @keyframes title-drop {
from { from {
opacity: 0; opacity: 0;
transform: translateY(-1rem); transform: translateX(-0.5rem);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateX(0);
} }
} }

View File

@ -18,6 +18,7 @@
--color-primary-hover: #000000; --color-primary-hover: #000000;
--color-secondary: #555; --color-secondary: #555;
--color-secondary-hover: #444; --color-secondary-hover: #444;
--color-cyan-50: oklch(98.97% 0.015 196.79); --color-cyan-50: oklch(98.97% 0.015 196.79);
--color-cyan-100: oklch(97.92% 0.03 196.61); --color-cyan-100: oklch(97.92% 0.03 196.61);
--color-cyan-200: oklch(95.79% 0.063 196.12); --color-cyan-200: oklch(95.79% 0.063 196.12);
@ -30,6 +31,19 @@
--color-cyan-800: oklch(59.15% 0.101 194.76); --color-cyan-800: oklch(59.15% 0.101 194.76);
--color-cyan-900: oklch(49.05% 0.084 194.76); --color-cyan-900: oklch(49.05% 0.084 194.76);
--color-cyan-950: oklch(43.96% 0.075 194.76); --color-cyan-950: oklch(43.96% 0.075 194.76);
--color-green-50: oklch(0.975 0.014 162.33);
--color-green-100: oklch(0.953 0.029 163.16);
--color-green-200: oklch(0.907 0.058 162.25);
--color-green-300: oklch(0.864 0.085 160.81);
--color-green-400: oklch(0.821 0.112 159.55);
--color-green-500: oklch(0.782 0.136 157.79); /* #61d296 as 500 */
--color-green-600: oklch(0.734 0.161 155.16);
--color-green-700: oklch(0.624 0.137 155.13);
--color-green-800: oklch(0.507 0.108 155.72);
--color-green-900: oklch(0.382 0.078 156.05);
--color-green-950: oklch(0.319 0.063 156.43);
--color-magenta-50: oklch(96.93% 0.027 325.87); --color-magenta-50: oklch(96.93% 0.027 325.87);
--color-magenta-100: oklch(93.76% 0.056 326.06); --color-magenta-100: oklch(93.76% 0.056 326.06);
--color-magenta-200: oklch(87.58% 0.117 326.54); --color-magenta-200: oklch(87.58% 0.117 326.54);
@ -42,6 +56,7 @@
--color-magenta-800: oklch(47.69% 0.219 328.37); --color-magenta-800: oklch(47.69% 0.219 328.37);
--color-magenta-900: oklch(40.42% 0.186 328.37); --color-magenta-900: oklch(40.42% 0.186 328.37);
--color-magenta-950: oklch(36.79% 0.169 328.37); --color-magenta-950: oklch(36.79% 0.169 328.37);
--color-red-50: oklch(0.975 0.012 23.84); --color-red-50: oklch(0.975 0.012 23.84);
--color-red-100: oklch(0.951 0.024 20.79); --color-red-100: oklch(0.951 0.024 20.79);
--color-red-200: oklch(0.895 0.055 23.81); --color-red-200: oklch(0.895 0.055 23.81);
@ -56,6 +71,8 @@
--border-width-md: 1.5px; --border-width-md: 1.5px;
--outline-width-md: 1.5px;
--radius-xs: 0.25rem; --radius-xs: 0.25rem;
--radius-sm: 0.375rem; --radius-sm: 0.375rem;
--radius-md: 0.6667rem; --radius-md: 0.6667rem;

View File

@ -2,6 +2,7 @@ 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;
--button-border: var(--color-primary); --button-border: var(--color-primary);
--button-accent: var(--color-primary-hover); --button-accent: var(--color-primary-hover);
@ -16,7 +17,11 @@ button,
} }
&:focus { &:focus {
box-shadow: none; @apply shadow-none;
}
&:active {
@apply shadow-none outline-none;
left: 2.5px; left: 2.5px;
top: 2.5px; top: 2.5px;
} }

View File

@ -7,7 +7,7 @@ dl.toasts {
@apply h-0 invisible overflow-hidden; @apply h-0 invisible overflow-hidden;
&.success + dd { &.success + dd {
@apply bg-green-500 text-white; @apply bg-green-500 text-primary;
} }
&.error + dd { &.error + dd {

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-undo2-icon lucide-undo-2"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"/></svg>

After

Width:  |  Height:  |  Size: 327 B

View File

@ -2,7 +2,10 @@
<x-slot name="aside"> <x-slot name="aside">
<h1> <h1>
{{ __('common.calendar') }} <a href="{{ route('calendar.index') }}" class="app-return">
<span>{{ __('common.calendar') }}</span>
<x-icon-return width="20" />
</a>
</h1> </h1>
<x-menu.calendar-settings :calendars="$calendars" /> <x-menu.calendar-settings :calendars="$calendars" />
</x-slot> </x-slot>

View File

@ -9,70 +9,67 @@
</p> </p>
</div> </div>
<form method="post" <form method="post" action="{{ route('calendar.settings.language.store') }}" class="settings">
action="{{ route('calendar.settings.language.store') }}"
class="form-grid-1 mt-8">
@csrf @csrf
<div> <div class="input-row input-row--1-1">
<label for="language">{{ __('Language') }}</label> <div class="input-cell">
<select id="language" name="language"> <x-input.label for="language" :value="__('common.language')" />
@foreach(($options['languages'] ?? []) as $value => $label) <x-input.select
<option value="{{ $value }}" @selected(old('language', $values['language'] ?? '') === $value)> id="language"
{{ $label }} name="language"
</option> placeholder="{{ __('common.language_select') }}"
@endforeach :value="$values['language']"
</select> :options="$options['languages']"
@error('language') :selected="old('language', $values['language'])"
<div class="text-danger">{{ $message }}</div> />
@enderror <x-input.error :messages="$errors->get('language')" />
</div>
<div class="input-cell">
<x-input.label for="region" :value="__('common.region')" />
<x-input.select
id="region"
name="region"
placeholder="{{ __('common.region_select') }}"
:value="$values['region']"
:options="$options['regions']"
:selected="old('region', $values['region'])"
/>
<x-input.error :messages="$errors->get('region')" />
</div>
</div> </div>
<div> <div class="input-row input-row--1-1">
<label for="region">{{ __('Region') }}</label> <div class="input-cell">
<select id="region" name="region"> <x-input.label for="date_format" :value="__('common.date_format')" />
@foreach(($options['regions'] ?? []) as $value => $label) <x-input.select
<option value="{{ $value }}" @selected(old('region', $values['region'] ?? '') === $value)> id="date_format"
{{ $label }} name="date_format"
</option> placeholder="{{ __('common.date_format_select') }}"
@endforeach :value="$values['date_format']"
</select> :options="$options['date_formats']"
@error('region') :selected="old('date_format', $values['date_format'])"
<div class="text-danger">{{ $message }}</div> />
@enderror <x-input.error :messages="$errors->get('date_format')" />
</div>
<div class="input-cell">
<x-input.label for="time_format" :value="__('common.time_format')" />
<x-input.select
id="time_format"
name="time_format"
placeholder="{{ __('common.time_format_select') }}"
:value="$values['time_format']"
:options="$options['time_formats']"
:selected="old('time_format', $values['time_format'])"
/>
<x-input.error :messages="$errors->get('time_format')" />
</div>
</div> </div>
<div> <div class="input-row input-row--actions input-row--start sticky-bottom">
<label for="date_format">{{ __('Date format') }}</label> <x-button variant="primary" type="submit">{{ __('common.save_changes') }}</x-button>
<select id="date_format" name="date_format"> <x-button type="anchor"
@foreach(($options['date_formats'] ?? []) as $value => $label) variant="tertiary"
<option value="{{ $value }}" @selected(old('date_format', $values['date_format'] ?? '') === $value)> href="{{ route('calendar.settings.language') }}">{{ __('common.cancel') }}</x-button>
{{ $label }}
</option>
@endforeach
</select>
@error('date_format')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<div>
<label for="time_format">{{ __('Time format') }}</label>
<select id="time_format" name="time_format">
@foreach(($options['time_formats'] ?? []) as $value => $label)
<option value="{{ $value }}" @selected(old('time_format', $values['time_format'] ?? '') === $value)>
{{ $label }}
</option>
@endforeach
</select>
@error('time_format')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<div class="flex gap-4">
<x-button variant="primary" type="submit">{{ __('Save') }}</x-button>
<a href="{{ route('calendar.index') }}"
class="button button--secondary">{{ __('Cancel and go back') }}</a>
</div> </div>
</form> </form>