generated from thegrind/laravel-dockerized
App icons and editing
This commit is contained in:
parent
4d1d6fbd29
commit
9746756a44
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -48,5 +48,5 @@ jobs:
|
||||
file: ${{ github.workspace }}/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
gitgud.foo/thegrind/flowtodo:latest
|
||||
gitgud.foo/thegrind/flowtodo:${{ github.event.release.tag_name }}
|
||||
gitgud.foo/thegrind/authentikate:latest
|
||||
gitgud.foo/thegrind/authentikate:${{ github.event.release.tag_name }}
|
58
app/Livewire/AppContainer.php
Normal file
58
app/Livewire/AppContainer.php
Normal 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');
|
||||
}
|
||||
}
|
57
app/Livewire/AppInfoModal.php
Normal file
57
app/Livewire/AppInfoModal.php
Normal 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');
|
||||
}
|
||||
}
|
@ -7,4 +7,9 @@ use Illuminate\Database\Eloquent\Model;
|
||||
class Application extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
|
||||
public function getIconUrl()
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
}
|
||||
|
28
database/migrations/2025_07_27_194829_add_icons_to_apps.php
Normal file
28
database/migrations/2025_07_27_194829_add_icons_to_apps.php
Normal 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');
|
||||
});
|
||||
}
|
||||
};
|
1
public/img/authentikate-logo-2.svg
Normal file
1
public/img/authentikate-logo-2.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 414 KiB |
54
public/img/logo.svg
Normal file
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 |
@ -1,6 +1,7 @@
|
||||
<div class="flex aspect-square size-8 items-center justify-center rounded-md bg-accent-content text-accent-foreground">
|
||||
<x-app-logo-icon class="size-5 fill-current text-white dark:text-black" />
|
||||
<div
|
||||
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 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>
|
@ -1,124 +1,102 @@
|
||||
<!DOCTYPE html>
|
||||
<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>
|
||||
<x-app-logo />
|
||||
</a>
|
||||
<head>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
|
||||
<flux:navbar class="-mb-px max-lg:hidden">
|
||||
<flux:navbar.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
|
||||
{{ __('Dashboard') }}
|
||||
</flux:navbar.item>
|
||||
</flux:navbar>
|
||||
<body class="min-h-screen bg-stone-200 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" />
|
||||
|
||||
<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:tooltip :content="__('Search')" position="bottom">
|
||||
<flux:navbar.item class="!h-10 [&>div>svg]:size-5" icon="magnifying-glass" href="#" :label="__('Search')" />
|
||||
</flux:tooltip>
|
||||
<flux:tooltip :content="__('Repository')" position="bottom">
|
||||
<flux:navbar.item
|
||||
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>
|
||||
<flux:navbar class="-mb-px max-lg:hidden">
|
||||
<flux:navbar.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')"
|
||||
wire:navigate>
|
||||
{{ __('Apps') }}
|
||||
</flux:navbar.item>
|
||||
</flux:navbar>
|
||||
|
||||
<!-- Desktop User Menu -->
|
||||
<flux:dropdown position="top" align="end">
|
||||
<flux:profile
|
||||
class="cursor-pointer"
|
||||
:initials="auth()->user()->initials()"
|
||||
/>
|
||||
<flux:spacer />
|
||||
|
||||
<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>
|
||||
<flux:navbar>
|
||||
<flux:radio.group x-data variant="segmented" x-model="$flux.appearance" size="sm">
|
||||
<flux:radio value="light" icon="sun" />
|
||||
<flux:radio value="dark" icon="moon" />
|
||||
<flux:radio value="system" icon="computer-desktop" />
|
||||
</flux:radio.group>
|
||||
</flux:navbar>
|
||||
|
||||
<!-- Desktop User Menu -->
|
||||
<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>
|
||||
|
||||
<div class="grid flex-1 text-start text-sm leading-tight">
|
||||
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
|
||||
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
|
||||
</div>
|
||||
<div class="grid flex-1 text-start text-sm leading-tight">
|
||||
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
|
||||
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</flux:menu.radio.group>
|
||||
</div>
|
||||
</flux:menu.radio.group>
|
||||
|
||||
<flux:menu.separator />
|
||||
<flux:menu.separator />
|
||||
|
||||
<flux:menu.radio.group>
|
||||
<flux:menu.item :href="route('settings.profile')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item>
|
||||
</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.radio.group>
|
||||
|
||||
<flux:menu.separator />
|
||||
<flux:menu.separator />
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}" class="w-full">
|
||||
@csrf
|
||||
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full">
|
||||
{{ __('Log Out') }}
|
||||
</flux:menu.item>
|
||||
</form>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</flux:header>
|
||||
<form method="POST" action="{{ route('logout') }}" class="w-full">
|
||||
@csrf
|
||||
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full">
|
||||
{{ __('Log Out') }}
|
||||
</flux:menu.item>
|
||||
</form>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</flux:header>
|
||||
|
||||
<!-- 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.toggle class="lg:hidden" icon="x-mark" />
|
||||
<!-- 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.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>
|
||||
<x-app-logo />
|
||||
</a>
|
||||
<a href="{{ route('dashboard') }}" class="ms-1 flex items-center space-x-2 rtl:space-x-reverse" wire:navigate>
|
||||
<x-app-logo />
|
||||
</a>
|
||||
|
||||
<flux:navlist variant="outline">
|
||||
<flux:navlist.group :heading="__('Platform')">
|
||||
<flux:navlist.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
|
||||
{{ __('Dashboard') }}
|
||||
</flux:navlist.item>
|
||||
</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 variant="outline">
|
||||
<flux:navlist.group :heading="__('Platform')">
|
||||
<flux:navlist.item icon="layout-grid" :href="route('dashboard')"
|
||||
:current="request()->routeIs('dashboard')" wire:navigate>
|
||||
{{ __('Dashboard') }}
|
||||
</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">
|
||||
{{ __('Documentation') }}
|
||||
</flux:navlist.item>
|
||||
</flux:navlist>
|
||||
</flux:sidebar>
|
||||
<flux:spacer />
|
||||
</flux:sidebar>
|
||||
|
||||
{{ $slot }}
|
||||
{{ $slot }}
|
||||
|
||||
@fluxScripts
|
||||
</body>
|
||||
|
||||
@fluxScripts
|
||||
</body>
|
||||
</html>
|
@ -1,14 +1,5 @@
|
||||
<x-layouts.app :title="__('Dashboard')">
|
||||
<div class="max-w-7xl mx-auto py-12">
|
||||
<livewire:forms.new-application />
|
||||
<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 class="max-w-4xl mx-auto py-12">
|
||||
<livewire:app-container />
|
||||
</div>
|
||||
</x-layouts.app>
|
34
resources/views/livewire/app-container.blade.php
Normal file
34
resources/views/livewire/app-container.blade.php
Normal 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>
|
39
resources/views/livewire/app-info-modal.blade.php
Normal file
39
resources/views/livewire/app-info-modal.blade.php
Normal 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>
|
@ -1,7 +1,9 @@
|
||||
<form wire:submit="create" class="flex flex-col gap-4">
|
||||
<flux:heading size="xl">Add an app</flux:heading>
|
||||
<flux:separator />
|
||||
<flux:input wire:model="name" label="App Name" placeholder="Linkwarden, NocoDB, etc" />
|
||||
<flux:input wire:model="redirect_uri" label="Redirect URI" placeholder="https://some.app/authorize" />
|
||||
<flux:button variant="primary" type="submit">Create</flux:button>
|
||||
</form>
|
||||
<flux:modal name="new-app">
|
||||
<form wire:submit="create" class="flex flex-col gap-4 md:w-96">
|
||||
<flux:heading size="xl">Add an app</flux:heading>
|
||||
<flux:separator />
|
||||
<flux:input wire:model="name" label="App Name" placeholder="Linkwarden, NocoDB, etc" />
|
||||
<flux:input wire:model="redirect_uri" label="Redirect URI" placeholder="https://some.app/authorize" />
|
||||
<flux:button variant="primary" type="submit">Create</flux:button>
|
||||
</form>
|
||||
</flux:modal>
|
Loading…
x
Reference in New Issue
Block a user