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

View File

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

View File

@ -26,7 +26,7 @@ return [
],
'language_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',
'subscribe' => [

View File

@ -17,11 +17,23 @@ return [
'calendars' => 'Calendars',
'cancel' => 'Cancel',
'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',
'events' => 'Events',
'language' => 'Language',
'language_select' => 'Select a language',
'password' => 'Password',
'region' => 'Region',
'region_select' => 'Select a region',
'save_changes' => 'Save changes',
'settings' => 'Settings',
'time' => 'Time',
'time_select' => 'Select a time',
'time_format' => 'Time format',
'time_format_select' => 'Select a time format',
'timezone' => '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 backdrop-blur-xs sticky top-0 z-1 shrink-0;
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 {
@ -248,11 +265,11 @@ main {
@keyframes title-drop {
from {
opacity: 0;
transform: translateY(-1rem);
transform: translateX(-0.5rem);
}
to {
opacity: 1;
transform: translateY(0);
transform: translateX(0);
}
}

View File

@ -18,6 +18,7 @@
--color-primary-hover: #000000;
--color-secondary: #555;
--color-secondary-hover: #444;
--color-cyan-50: oklch(98.97% 0.015 196.79);
--color-cyan-100: oklch(97.92% 0.03 196.61);
--color-cyan-200: oklch(95.79% 0.063 196.12);
@ -30,6 +31,19 @@
--color-cyan-800: oklch(59.15% 0.101 194.76);
--color-cyan-900: oklch(49.05% 0.084 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-100: oklch(93.76% 0.056 326.06);
--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-900: oklch(40.42% 0.186 328.37);
--color-magenta-950: oklch(36.79% 0.169 328.37);
--color-red-50: oklch(0.975 0.012 23.84);
--color-red-100: oklch(0.951 0.024 20.79);
--color-red-200: oklch(0.895 0.055 23.81);
@ -56,6 +71,8 @@
--border-width-md: 1.5px;
--outline-width-md: 1.5px;
--radius-xs: 0.25rem;
--radius-sm: 0.375rem;
--radius-md: 0.6667rem;

View File

@ -2,6 +2,7 @@ button,
.button {
@apply relative inline-flex items-center cursor-pointer gap-2 rounded-md h-11 px-4 text-lg font-medium;
/*transition: background-color 125ms ease-in-out; */
@apply focus:outline-md focus:outline-offset-2 focus:outline-secondary;
--button-border: var(--color-primary);
--button-accent: var(--color-primary-hover);
@ -16,7 +17,11 @@ button,
}
&:focus {
box-shadow: none;
@apply shadow-none;
}
&:active {
@apply shadow-none outline-none;
left: 2.5px;
top: 2.5px;
}

View File

@ -7,7 +7,7 @@ dl.toasts {
@apply h-0 invisible overflow-hidden;
&.success + dd {
@apply bg-green-500 text-white;
@apply bg-green-500 text-primary;
}
&.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">
<h1>
{{ __('common.calendar') }}
<a href="{{ route('calendar.index') }}" class="app-return">
<span>{{ __('common.calendar') }}</span>
<x-icon-return width="20" />
</a>
</h1>
<x-menu.calendar-settings :calendars="$calendars" />
</x-slot>

View File

@ -9,70 +9,67 @@
</p>
</div>
<form method="post"
action="{{ route('calendar.settings.language.store') }}"
class="form-grid-1 mt-8">
<form method="post" action="{{ route('calendar.settings.language.store') }}" class="settings">
@csrf
<div>
<label for="language">{{ __('Language') }}</label>
<select id="language" name="language">
@foreach(($options['languages'] ?? []) as $value => $label)
<option value="{{ $value }}" @selected(old('language', $values['language'] ?? '') === $value)>
{{ $label }}
</option>
@endforeach
</select>
@error('language')
<div class="text-danger">{{ $message }}</div>
@enderror
<div class="input-row input-row--1-1">
<div class="input-cell">
<x-input.label for="language" :value="__('common.language')" />
<x-input.select
id="language"
name="language"
placeholder="{{ __('common.language_select') }}"
:value="$values['language']"
:options="$options['languages']"
:selected="old('language', $values['language'])"
/>
<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>
<label for="region">{{ __('Region') }}</label>
<select id="region" name="region">
@foreach(($options['regions'] ?? []) as $value => $label)
<option value="{{ $value }}" @selected(old('region', $values['region'] ?? '') === $value)>
{{ $label }}
</option>
@endforeach
</select>
@error('region')
<div class="text-danger">{{ $message }}</div>
@enderror
<div class="input-row input-row--1-1">
<div class="input-cell">
<x-input.label for="date_format" :value="__('common.date_format')" />
<x-input.select
id="date_format"
name="date_format"
placeholder="{{ __('common.date_format_select') }}"
:value="$values['date_format']"
:options="$options['date_formats']"
:selected="old('date_format', $values['date_format'])"
/>
<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>
<label for="date_format">{{ __('Date format') }}</label>
<select id="date_format" name="date_format">
@foreach(($options['date_formats'] ?? []) as $value => $label)
<option value="{{ $value }}" @selected(old('date_format', $values['date_format'] ?? '') === $value)>
{{ $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 class="input-row input-row--actions input-row--start sticky-bottom">
<x-button variant="primary" type="submit">{{ __('common.save_changes') }}</x-button>
<x-button type="anchor"
variant="tertiary"
href="{{ route('calendar.settings.language') }}">{{ __('common.cancel') }}</x-button>
</div>
</form>