App icons and editing
Some checks failed
linter / quality (push) Successful in 3m5s
tests / ci (push) Failing after 7m35s

This commit is contained in:
Javier Feliz 2025-07-27 16:57:02 -04:00
parent 4d1d6fbd29
commit 9746756a44
14 changed files with 403 additions and 133 deletions

View File

@ -48,5 +48,5 @@ jobs:
file: ${{ github.workspace }}/Dockerfile file: ${{ github.workspace }}/Dockerfile
push: true push: true
tags: | tags: |
gitgud.foo/thegrind/flowtodo:latest gitgud.foo/thegrind/authentikate:latest
gitgud.foo/thegrind/flowtodo:${{ github.event.release.tag_name }} gitgud.foo/thegrind/authentikate:${{ github.event.release.tag_name }}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Livewire;
use App\Models\Application;
use Flux\Flux;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Attributes\On;
use Livewire\Component;
class AppContainer extends Component
{
public Collection $apps;
public ?Application $confirmDeleteApp;
public function mount()
{
$this->loadApps();
}
public function loadApps()
{
$this->apps = Application::orderBy('id')->get();
}
#[On('app-updated')]
public function handleAppUpdated($id)
{
$new = Application::find($id);
$this->apps = $this->apps->map(fn($app) => $app->id == $id ? $new : $app);
}
public function confirmDelete($id)
{
$this->confirmDeleteApp = $this->apps->where('id', $id)->first();
Flux::modal('delete-app-confirm')->show();
}
public function deleteApp()
{
$this->confirmDeleteApp->delete();
$deletedId = $this->confirmDeleteApp->id;
$this->confirmDeleteApp = null;
$this->apps = $this->apps->filter(fn($app) => $app->id != $deletedId);
Flux::modal('delete-app-confirm')->close();
}
public function cancelDelete()
{
$this->confirmDeleteApp = null;
Flux::modal('delete-app-confirm')->close();
}
public function render()
{
return view('livewire.app-container');
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Livewire;
use App\Models\Application;
use Flux\Flux;
use Livewire\Attributes\Computed;
use Livewire\Attributes\On;
use Livewire\Component;
class AppInfoModal extends Component
{
public Application $app;
public ?string $name = '';
public string $query = '';
public ?string $icon = null;
public function mount()
{
$this->loadApp(4);
}
public function updated($prop)
{
if ($prop == "query") {
if (empty($this->query)) {
return null;
}
$s = str($this->query)->kebab()->toString();
$icon = "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/{$s}.webp";
$this->icon = $icon;
}
}
#[On('appinfo')]
public function loadApp($id)
{
$this->app = Application::find($id);
$this->name = $this->app->name;
$this->icon = $this->app->getIconUrl();
}
public function save()
{
$this->app->update([
'name' => $this->name,
'icon' => $this->icon,
]);
$this->dispatch('app-updated', ['id' => $this->app->id]);
}
public function render()
{
return view('livewire.app-info-modal');
}
}

View File

@ -7,4 +7,9 @@ use Illuminate\Database\Eloquent\Model;
class Application extends Model class Application extends Model
{ {
protected $guarded = ['id']; protected $guarded = ['id'];
public function getIconUrl()
{
return $this->icon;
}
} }

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->string('icon')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('icon');
});
}
};

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 414 KiB

54
public/img/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 414 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 714 B

After

Width:  |  Height:  |  Size: 414 KiB

View File

@ -1,6 +1,7 @@
<div class="flex aspect-square size-8 items-center justify-center rounded-md bg-accent-content text-accent-foreground"> <div
<x-app-logo-icon class="size-5 fill-current text-white dark:text-black" /> class="flex aspect-square size-10 items-center justify-center rounded-md dark:bg-accent-content text-accent-foreground">
<x-app-logo-icon class="size-10 fill-current text-white dark:text-black" />
</div> </div>
<div class="ms-1 grid flex-1 text-start text-sm"> <div class="ms-1 grid flex-1 text-start text-sm">
<span class="mb-0.5 truncate leading-tight font-semibold">Laravel Starter Kit</span> <span class="mb-0.5 truncate leading-tight font-semibold">{{ config('app.name') }}</span>
</div> </div>

View File

@ -1,124 +1,102 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark"> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white dark:bg-zinc-800">
<flux:header container class="border-b border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />
<a href="{{ route('dashboard') }}" class="ms-2 me-5 flex items-center space-x-2 rtl:space-x-reverse lg:ms-0" wire:navigate> <head>
<x-app-logo /> @include('partials.head')
</a> </head>
<flux:navbar class="-mb-px max-lg:hidden"> <body class="min-h-screen bg-stone-200 dark:bg-zinc-800">
<flux:navbar.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate> <flux:header container class="border-b border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
{{ __('Dashboard') }} <flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />
</flux:navbar.item>
</flux:navbar>
<flux:spacer /> <a href="{{ route('dashboard') }}" class="ms-2 me-5 flex items-center space-x-2 rtl:space-x-reverse lg:ms-0"
wire:navigate>
<x-app-logo />
</a>
<flux:navbar class="me-1.5 space-x-0.5 rtl:space-x-reverse py-0!"> <flux:navbar class="-mb-px max-lg:hidden">
<flux:tooltip :content="__('Search')" position="bottom"> <flux:navbar.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')"
<flux:navbar.item class="!h-10 [&>div>svg]:size-5" icon="magnifying-glass" href="#" :label="__('Search')" /> wire:navigate>
</flux:tooltip> {{ __('Apps') }}
<flux:tooltip :content="__('Repository')" position="bottom"> </flux:navbar.item>
<flux:navbar.item </flux:navbar>
class="h-10 max-lg:hidden [&>div>svg]:size-5"
icon="folder-git-2"
href="https://github.com/laravel/livewire-starter-kit"
target="_blank"
:label="__('Repository')"
/>
</flux:tooltip>
<flux:tooltip :content="__('Documentation')" position="bottom">
<flux:navbar.item
class="h-10 max-lg:hidden [&>div>svg]:size-5"
icon="book-open-text"
href="https://laravel.com/docs/starter-kits#livewire"
target="_blank"
label="Documentation"
/>
</flux:tooltip>
</flux:navbar>
<!-- Desktop User Menu --> <flux:spacer />
<flux:dropdown position="top" align="end">
<flux:profile
class="cursor-pointer"
:initials="auth()->user()->initials()"
/>
<flux:menu> <flux:navbar>
<flux:menu.radio.group> <flux:radio.group x-data variant="segmented" x-model="$flux.appearance" size="sm">
<div class="p-0 text-sm font-normal"> <flux:radio value="light" icon="sun" />
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm"> <flux:radio value="dark" icon="moon" />
<span class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-lg"> <flux:radio value="system" icon="computer-desktop" />
<span </flux:radio.group>
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white" </flux:navbar>
>
{{ auth()->user()->initials() }} <!-- Desktop User Menu -->
</span> <flux:dropdown position="top" align="end">
<flux:profile class="cursor-pointer" :initials="auth()->user()->initials()" />
<flux:menu>
<flux:menu.radio.group>
<div class="p-0 text-sm font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
<span class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-lg">
<span
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white">
{{ auth()->user()->initials() }}
</span> </span>
</span>
<div class="grid flex-1 text-start text-sm leading-tight"> <div class="grid flex-1 text-start text-sm leading-tight">
<span class="truncate font-semibold">{{ auth()->user()->name }}</span> <span class="truncate font-semibold">{{ auth()->user()->name }}</span>
<span class="truncate text-xs">{{ auth()->user()->email }}</span> <span class="truncate text-xs">{{ auth()->user()->email }}</span>
</div>
</div> </div>
</div> </div>
</flux:menu.radio.group> </div>
</flux:menu.radio.group>
<flux:menu.separator /> <flux:menu.separator />
<flux:menu.radio.group> <flux:menu.radio.group>
<flux:menu.item :href="route('settings.profile')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item> <flux:menu.item :href="route('settings.profile')" icon="cog" wire:navigate>{{ __('Settings') }}
</flux:menu.radio.group> </flux:menu.item>
</flux:menu.radio.group>
<flux:menu.separator /> <flux:menu.separator />
<form method="POST" action="{{ route('logout') }}" class="w-full"> <form method="POST" action="{{ route('logout') }}" class="w-full">
@csrf @csrf
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full"> <flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full">
{{ __('Log Out') }} {{ __('Log Out') }}
</flux:menu.item> </flux:menu.item>
</form> </form>
</flux:menu> </flux:menu>
</flux:dropdown> </flux:dropdown>
</flux:header> </flux:header>
<!-- Mobile Menu --> <!-- Mobile Menu -->
<flux:sidebar stashable sticky class="lg:hidden border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900"> <flux:sidebar stashable sticky
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" /> class="lg:hidden border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
<a href="{{ route('dashboard') }}" class="ms-1 flex items-center space-x-2 rtl:space-x-reverse" wire:navigate> <a href="{{ route('dashboard') }}" class="ms-1 flex items-center space-x-2 rtl:space-x-reverse" wire:navigate>
<x-app-logo /> <x-app-logo />
</a> </a>
<flux:navlist variant="outline"> <flux:navlist variant="outline">
<flux:navlist.group :heading="__('Platform')"> <flux:navlist.group :heading="__('Platform')">
<flux:navlist.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate> <flux:navlist.item icon="layout-grid" :href="route('dashboard')"
{{ __('Dashboard') }} :current="request()->routeIs('dashboard')" wire:navigate>
</flux:navlist.item> {{ __('Dashboard') }}
</flux:navlist.group>
</flux:navlist>
<flux:spacer />
<flux:navlist variant="outline">
<flux:navlist.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
{{ __('Repository') }}
</flux:navlist.item> </flux:navlist.item>
</flux:navlist.group>
</flux:navlist>
<flux:navlist.item icon="book-open-text" href="https://laravel.com/docs/starter-kits#livewire" target="_blank"> <flux:spacer />
{{ __('Documentation') }} </flux:sidebar>
</flux:navlist.item>
</flux:navlist>
</flux:sidebar>
{{ $slot }} {{ $slot }}
@fluxScripts @fluxScripts
</body> </body>
</html>
</html>

View File

@ -1,14 +1,5 @@
<x-layouts.app :title="__('Dashboard')"> <x-layouts.app :title="__('Dashboard')">
<div class="max-w-7xl mx-auto py-12"> <div class="max-w-4xl mx-auto py-12">
<livewire:forms.new-application /> <livewire:app-container />
<div class="mt-4 grid grid-cols-3">
@foreach (App\Models\Application::all() as $app)
<div class="p-4 flex flex-col gap-4 rounded-md bg-light-200 dark:bg-zinc-700">
<flux:heading>{{ $app->name}}</flux:heading>
<flux:text>Client ID: {{ $app->client_id }}</flux:text>
<flux:text>Secret: {{ $app->client_secret }}</flux:text>
</div>
@endforeach
</div>
</div> </div>
</x-layouts.app> </x-layouts.app>

View File

@ -0,0 +1,34 @@
<div>
<div class="flex items-center justify-end mb-8">
<flux:modal.trigger name="new-app">
<flux:button variant="primary" icon="plus">New App</flux:button>
</flux:modal.trigger>
</div>
<div class="mt-4 grid grid-cols-4 gap-8">
@foreach ($apps as $app)
<div class="p-4 flex flex-col gap-4 rounded-md bg-gray-100 dark:bg-zinc-700">
<div class="flex items-center justify-between">
<flux:modal.trigger name="app-info">
<flux:button icon="eye" variant="subtle" size="sm" inset class="cursor-pointer"
x-on:click="$dispatch('appinfo', {id: {{$app->id}}})" />
</flux:modal.trigger>
<flux:button wire:click="confirmDelete({{$app->id}})" icon="trash" variant="subtle" size="sm" inset
class="cursor-pointer" />
</div>
<img src="{{$app->getIconUrl()}}" alt="" class="size-32 mx-auto">
<flux:heading size="lg" class="text-center">{{ $app->name}}</flux:heading>
</div>
@endforeach
</div>
<flux:modal name="delete-app-confirm" class="md:w-96">
@if ($confirmDeleteApp)
<flux:heading size="lg" class="mb-8">Delete {{$confirmDeleteApp->name}}?</flux:heading>
<div class="flex gap-4">
<flux:button variant="primary" color="red" class="flex-1" wire:click="deleteApp">Delete</flux:button>
<flux:button variant="primary" class="flex-1" wire:click="cancelDelete">Cancel</flux:button>
</div>
@endif
</flux:modal>
<livewire:app-info-modal />
<livewire:forms.new-application />
</div>

View File

@ -0,0 +1,39 @@
<flux:modal variant="flyout" position="left" name="app-info" class="md:w-96">
@if (empty($app))
<flux:text>I'm not sure how you got here, but no app is loaded</flux:text>
@else
<div class="space-y-6 pt-4">
<div x-data="{edit: false}">
<div x-show="!edit"
class="flex items-center gap-4 border dark:border-zinc-600 border-stone-300 rounded-sm p-2">
<img src="{{$app->getIconUrl()}}" alt="" class="size-24">
<flux:heading size="xl">{{$app->name}}</flux:heading>
</div>
<div class="flex justify-end" x-show="!edit">
<flux:button x-on:click="edit = true" variant="subtle" icon="pencil">Edit</flux:button>
</div>
<form x-show="edit" wire:submit="save" class="flex flex-col gap-4">
<div>
<img src="{{$icon}}" alt="" class="size-24 mx-auto">
<flux:input wire:model.live="query" label="Icon" />
</div>
<flux:input wire:model="name" />
<div class="flex gap-4">
<flux:button class="flex-1" variant="primary" type="submit" x-on:click="edit = false">Save
</flux:button>
<flux:button class="flex-1" variant="subtle" color="red" x-on:click.prevent="edit = false">Cancel
</flux:button>
</div>
</form>
</div>
<flux:separator text="Connection Information" />
<flux:input label="Client ID" disabled value="{{$app->client_id}}" copyable />
<flux:input label="Client Secret" disabled value="{{$app->client_secret}}" copyable />
<flux:input label="Authorization Endpoint" disabled value="{{route('auth.authorize')}}" copyable />
<flux:input label="Token Endpoint" disabled value="{{route('auth.token')}}" copyable />
<flux:input label="User Endpoint" disabled value="{{route('auth.userinfo')}}" copyable />
<flux:input label="Scopes" disabled value="openid email profile" copyable />
<flux:input label="Identifier" disabled value="email" copyable />
</div>
@endif
</flux:modal>

View File

@ -1,7 +1,9 @@
<form wire:submit="create" class="flex flex-col gap-4"> <flux:modal name="new-app">
<flux:heading size="xl">Add an app</flux:heading> <form wire:submit="create" class="flex flex-col gap-4 md:w-96">
<flux:separator /> <flux:heading size="xl">Add an app</flux:heading>
<flux:input wire:model="name" label="App Name" placeholder="Linkwarden, NocoDB, etc" /> <flux:separator />
<flux:input wire:model="redirect_uri" label="Redirect URI" placeholder="https://some.app/authorize" /> <flux:input wire:model="name" label="App Name" placeholder="Linkwarden, NocoDB, etc" />
<flux:button variant="primary" type="submit">Create</flux:button> <flux:input wire:model="redirect_uri" label="Redirect URI" placeholder="https://some.app/authorize" />
</form> <flux:button variant="primary" type="submit">Create</flux:button>
</form>
</flux:modal>