This commit is contained in:
Andras Bacsai 2023-06-15 10:48:13 +02:00
parent fe51f8fbf7
commit 8b6598ea6b
22 changed files with 380 additions and 231 deletions

View File

@ -59,7 +59,7 @@ public function instantSave()
$this->application->settings->is_force_https_enabled = $this->is_force_https_enabled;
$this->application->settings->save();
$this->application->refresh();
$this->emit('saved', 'Application settings updated!');
$this->emit('success', 'Application settings updated!');
$this->checkWildCardDomain();
}
protected function checkWildCardDomain()

View File

@ -40,6 +40,6 @@ public function submit()
$this->application->git_commit_sha = 'HEAD';
}
$this->application->save();
$this->emit('saved', 'Application source updated!');
$this->emit('success', 'Application source updated!');
}
}

View File

@ -157,6 +157,6 @@ public function instantSave()
$this->port = 3000;
$this->publish_directory = null;
}
$this->emit('saved', 'Application settings updated!');
$this->emit('success', 'Application settings updated!');
}
}

View File

@ -56,7 +56,7 @@ public function instantSave()
$this->port = 3000;
$this->publish_directory = null;
}
$this->emit('saved', 'Application settings updated!');
$this->emit('success', 'Application settings updated!');
}
public function load_branches()
{

View File

@ -39,7 +39,7 @@ public function setPrivateKey(string $private_key_id)
}
public function instantSave()
{
$this->emit('saved', 'Application settings updated!');
$this->emit('success', 'Application settings updated!');
}
public function submit()
{

View File

@ -37,7 +37,7 @@ public function instantSave()
$this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
$this->settings->is_registration_enabled = $this->is_registration_enabled;
$this->settings->save();
$this->emit('saved', 'Settings updated!');
$this->emit('success', 'Settings updated!');
}
private function setup_instance_fqdn()
{

View File

@ -59,9 +59,9 @@ private function generate_invite_link(bool $isEmail = false)
]);
if ($isEmail) {
$user->first()->notify(new InvitationLinkEmail());
$this->emit('message', 'Invitation sent via email successfully.');
$this->emit('success', 'Invitation sent via email successfully.');
} else {
$this->emit('message', 'Invitation link generated.');
$this->emit('success', 'Invitation link generated.');
}
$this->emit('refreshInvitations');
} catch (\Throwable $e) {

View File

@ -2,17 +2,19 @@
namespace App\Http\Livewire;
use Masmerise\Toaster\Toaster;
use App\Jobs\InstanceAutoUpdateJob;
use Livewire\Component;
class ForceUpgrade extends Component
class Upgrade extends Component
{
public bool $visible = false;
public bool $showProgress = false;
public function upgrade()
{
try {
$this->visible = true;
$this->showProgress = true;
dispatch(new InstanceAutoUpdateJob(force: true));
Toaster::success('Update started.');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
}

View File

@ -1,15 +0,0 @@
<?php
namespace App\Http\Livewire;
use Livewire\Component;
class Upgrading extends Component
{
public bool $visible = false;
protected $listeners = ['updateInitiated'];
public function updateInitiated()
{
$this->visible = true;
}
}

View File

@ -27,24 +27,6 @@ class InstanceAutoUpdateJob implements ShouldQueue, ShouldBeUnique
public function __construct(private bool $force = false)
{
}
private function update()
{
if (config('app.env') === 'local') {
ray('Running update on local docker container');
instant_remote_process([
"sleep 10"
], $this->server);
ray('Update done');
return;
} else {
ray('Running update on production server');
instant_remote_process([
"curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh",
"bash /data/coolify/source/upgrade.sh $this->latest_version"
], $this->server);
return;
}
}
public function handle(): void
{
try {
@ -81,6 +63,25 @@ public function handle(): void
return;
}
}
private function update()
{
if (config('app.env') === 'local') {
ray('Running update on local docker container');
instant_remote_process([
"sleep 10"
], $this->server);
ray('Update done');
return;
} else {
ray('Running update on production server');
instant_remote_process([
"curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh",
"bash /data/coolify/source/upgrade.sh $this->latest_version"
], $this->server);
return;
}
}
public function failed(Exception $exception)
{
return;

View File

@ -19,6 +19,7 @@
"laravel/ui": "^4.2",
"lcobucci/jwt": "^5.0.0",
"livewire/livewire": "^v2.12.3",
"masmerise/livewire-toaster": "^1.2",
"nubs/random-name-generator": "^2.2",
"sentry/sentry-laravel": "^3.4",
"spatie/laravel-activitylog": "^4.7.3",
@ -91,4 +92,4 @@
},
"minimum-stability": "stable",
"prefer-stable": true
}
}

70
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "77b718b33a7ba99083fd327821f968a2",
"content-hash": "d9173515bca399807784102128591e1e",
"packages": [
{
"name": "bacon/bacon-qr-code",
@ -2738,6 +2738,74 @@
],
"time": "2023-03-03T20:12:38+00:00"
},
{
"name": "masmerise/livewire-toaster",
"version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/masmerise/livewire-toaster.git",
"reference": "2706d3822e111af8272ebc117cb2c69228d14faf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/masmerise/livewire-toaster/zipball/2706d3822e111af8272ebc117cb2c69228d14faf",
"reference": "2706d3822e111af8272ebc117cb2c69228d14faf",
"shasum": ""
},
"require": {
"laravel/framework": "^10.0",
"livewire/livewire": "^2.0",
"php": "~8.2"
},
"require-dev": {
"dive-be/php-crowbar": "^1.1",
"laravel/pint": "^1.0",
"nunomaduro/larastan": "^2.0",
"orchestra/testbench": "^8.0",
"phpunit/phpunit": "^10.0"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Toaster": "Masmerise\\Toaster\\Toaster"
},
"providers": [
"Masmerise\\Toaster\\ToasterServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Masmerise\\Toaster\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Muhammed Sari",
"email": "support@muhammedsari.me",
"role": "Developer"
}
],
"description": "Beautiful toast notifications for Laravel / Livewire.",
"homepage": "https://github.com/masmerise/livewire-toaster",
"keywords": [
"alert",
"laravel",
"livewire",
"toast",
"toaster"
],
"support": {
"issues": "https://github.com/masmerise/livewire-toaster/issues",
"source": "https://github.com/masmerise/livewire-toaster/tree/1.2.0"
},
"time": "2023-06-13T11:44:44+00:00"
},
{
"name": "monolog/monolog",
"version": "3.3.1",

48
config/toaster.php Normal file
View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
return [
/**
* Add an additional second for every 100th word of the toast messages.
*
* Supported: true | false
*/
'accessibility' => true,
/**
* The vertical alignment of the toast container.
*
* Supported: "bottom", "middle" or "top"
*/
'alignment' => 'top',
/**
* Allow users to close toast messages prematurely.
*
* Supported: true | false
*/
'closeable' => true,
/**
* The on-screen duration of each toast.
*
* Minimum: 3000 (in milliseconds)
*/
'duration' => 3000,
/**
* The horizontal position of each toast.
*
* Supported: "center", "left" or "right"
*/
'position' => 'right',
/**
* Whether messages passed as translation keys should be translated automatically.
*
* Supported: true | false
*/
'translate' => true,
];

View File

@ -2,6 +2,10 @@ import Alpine from "alpinejs";
import { createApp } from "vue";
import MagicBar from "./components/MagicBar.vue";
import Toaster from "../../vendor/masmerise/livewire-toaster/resources/js";
Alpine.plugin(Toaster);
window.Alpine = Alpine;
Alpine.start();

View File

@ -26,6 +26,7 @@
<body>
@livewireScripts
<x-toaster-hub />
@auth
<x-navbar />
@endauth
@ -54,17 +55,17 @@ function copyToClipboard(text) {
Livewire.on('reloadWindow', () => {
window.location.reload();
})
Livewire.on('info', (message) => {
if (message) Toaster.info(message)
})
Livewire.on('error', (message) => {
console.log(message);
alert(message);
if (message) Toaster.error(message)
})
Livewire.on('message', (message) => {
console.log(message);
alert(message);
Livewire.on('warning', (message) => {
if (message) Toaster.warning(message)
})
Livewire.on('saved', (message) => {
if (message) console.log(message);
else console.log('saved');
Livewire.on('success', (message) => {
if (message) Toaster.success(message)
})
</script>
@else

View File

@ -67,6 +67,9 @@ class="{{ request()->is('settings*') ? 'text-warning icon' : 'icon' }}" viewBox=
</svg>
</a>
</li>
<li title="New version available">
<livewire:upgrade />
</li>
@endif
</ul>
</nav>

View File

@ -1,63 +0,0 @@
<div class="flex gap-10 text-xs text-white" x-data="{ visible: @entangle('visible') }">
<x-forms.button x-cloak x-show="!visible" wire:click='upgrade'>
Force Upgrade Your Instance
</x-forms.button>
<template x-if="visible">
<div class="bg-coollabs-gradient">
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 mx-auto text-pink-500 lds-heart" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M19.5 13.572l-7.5 7.428l-7.5 -7.428m0 0a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" />
</svg> Upgrading, please wait...
<script>
function checkHealth() {
console.log('Checking server\'s health...')
checkHealthInterval = setInterval(async () => {
try {
const res = await fetch('/api/health');
if (res.ok) {
console.log('Server is back online. Reloading...')
if (checkHealthInterval) clearInterval(checkHealthInterval);
window.location.reload();
}
} catch (error) {
console.log('Waiting for server to come back from dead...');
}
return;
}, 2000);
}
function checkIfIamDead() {
console.log('Checking server\'s pulse...')
checkIfIamDeadInterval = setInterval(async () => {
try {
const res = await fetch('/api/health');
if (res.ok) {
console.log('I\'m alive. Waiting for server to be dead...');
}
} catch (error) {
console.log('I\'m dead. Charging... Standby... Bzz... Bzz...')
checkHealth();
if (checkIfIamDeadInterval) clearInterval(checkIfIamDeadInterval);
}
return;
}, 2000);
}
let checkHealthInterval = null;
let checkIfIamDeadInterval = null;
console.log('Update initiated. Waiting for server to be dead...')
checkIfIamDead();
</script>
</div>
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8 mx-auto lds-heart" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M19.5 12.572l-7.5 7.428l-7.5 -7.428a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" />
<path d="M12 6l-2 4l4 3l-2 4v3" />
</svg>
</template>
{{-- <livewire:upgrading /> --}}
</div>

View File

@ -0,0 +1,67 @@
<div x-data wire:click='upgrade' x-on:click="upgrade" @class([
'bg-gradient-to-r from-purple-500 via-pink-500 to-red-500' => !$showProgress,
'hover:bg-transparent focus:bg-transparent' => $showProgress,
])>
<button>
@if ($showProgress)
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-pink-500 lds-heart" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M19.5 13.572l-7.5 7.428l-7.5 -7.428m0 0a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" />
</svg>
@else
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-white " viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M9 12h-3.586a1 1 0 0 1 -.707 -1.707l6.586 -6.586a1 1 0 0 1 1.414 0l6.586 6.586a1 1 0 0 1 -.707 1.707h-3.586v3h-6v-3z" />
<path d="M9 21h6" />
<path d="M9 18h6" />
</svg>
@endif
</button>
<script>
let checkHealthInterval = null;
let checkIfIamDeadInterval = null;
function revive() {
console.log('Checking server\'s health...')
checkHealthInterval = setInterval(() => {
fetch('/api/health')
.then(response => {
if (response.ok) {
console.log('Server is back online. Reloading...')
setTimeout(() => {
Toaster.success('Coolify started. Reloading!')
if (checkHealthInterval) clearInterval(checkHealthInterval);
window.location.reload();
}, 2000)
}
})
.catch(error => {
console.log('Waiting for server to come back from dead...');
});
return;
}, 5000);
}
function upgrade() {
console.log('Update initiated.')
checkIfIamDeadInterval = setInterval(() => {
fetch('/api/health')
.then(response => {
if (response.ok) {
console.log('It\'s alive. Waiting for server to be dead...');
}
})
.catch(error => {
Toaster.success('Update done, restart Coolify!')
console.log('It\'s dead. Reviving... Standby... Bzz... Bzz...')
revive();
if (checkIfIamDeadInterval) clearInterval(checkIfIamDeadInterval);
});
return;
}, 5000);
}
</script>
</div>

View File

@ -1,58 +0,0 @@
<div x-data="{ visible: @entangle('visible') }" class="flex text-xs text-white">
<template x-if="visible">
<div class="bg-coollabs-gradient">
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 mx-auto text-pink-500 lds-heart" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M19.5 13.572l-7.5 7.428l-7.5 -7.428m0 0a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" />
</svg> Upgrading, please wait...
<script>
function checkHealth() {
console.log('Checking server\'s health...')
checkHealthInterval = setInterval(async () => {
try {
const res = await fetch('/api/health');
if (res.ok) {
console.log('Server is back online. Reloading...')
if (checkHealthInterval) clearInterval(checkHealthInterval);
window.location.reload();
}
} catch (error) {
console.log('Waiting for server to come back from dead...');
}
return;
}, 2000);
}
function checkIfIamDead() {
console.log('Checking server\'s pulse...')
checkIfIamDeadInterval = setInterval(async () => {
try {
const res = await fetch('/api/health');
if (res.ok) {
console.log('I\'m alive. Waiting for server to be dead...');
}
} catch (error) {
console.log('I\'m dead. Charging... Standby... Bzz... Bzz...')
checkHealth();
if (checkIfIamDeadInterval) clearInterval(checkIfIamDeadInterval);
}
return;
}, 2000);
}
let checkHealthInterval = null;
let checkIfIamDeadInterval = null;
console.log('Update initiated. Waiting for server to be dead...')
checkIfIamDead();
</script>
</div>
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8 mx-auto lds-heart" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M19.5 12.572l-7.5 7.428l-7.5 -7.428a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" />
<path d="M12 6l-2 4l4 3l-2 4v3" />
</svg>
</template>
</div>

View File

@ -1,8 +1,4 @@
<x-layout>
<x-settings.navbar />
<livewire:settings.configuration :settings="$settings" />
@if (auth()->user()->isInstanceAdmin())
<livewire:force-upgrade />
@endif
</x-layout>

View File

@ -0,0 +1,90 @@
<div role="status" id="toaster" x-data="toasterHub(@js($toasts), @js($config))" @class([
'fixed z-50 p-4 w-full flex flex-col pointer-events-none sm:p-6',
'bottom-0' => $alignment->is('bottom'),
'top-1/2 -translate-y-1/2' => $alignment->is('middle'),
'top-0' => $alignment->is('top'),
'items-start' => $position->is('left'),
'items-center' => $position->is('center'),
'items-end' => $position->is('right'),
])>
<template x-for="toast in toasts" :key="toast.id">
<div x-show="toast.isVisible" x-init="$nextTick(() => toast.show($el))" @if ($alignment->is('bottom'))
x-transition:enter-start="translate-y-12 opacity-0"
x-transition:enter-end="translate-y-0 opacity-100"
@elseif($alignment->is('top'))
x-transition:enter-start="-translate-y-12 opacity-0"
x-transition:enter-end="translate-y-0 opacity-100"
@else
x-transition:enter-start="opacity-0 scale-90"
x-transition:enter-end="opacity-100 scale-100"
@endif
x-transition:leave-end="opacity-0 scale-90"
class="relative flex duration-300 transform transition ease-in-out max-w-md w-full pointer-events-auto {{ $position->is('center') ? 'text-center' : 'text-left' }}"
:class="toast.select({ error: 'text-white', info: 'text-white', success: 'text-white', warning: 'text-white' })"
>
<i class=" flex items-center gap-2 select-none not-italic pr-6 pl-4 py-3 rounded shadow-lg text-sm w-full {{ $alignment->is('bottom') ? 'mt-3' : 'mb-3' }}"
:class="toast.select({
error: 'bg-coolgray-300',
info: 'bg-coolgray-300',
success: 'bg-coolgray-300',
warning: 'bg-coolgray-300'
})">
<template x-if="toast.type === 'success'">
<div class="rounded text-success">
<svg aria-hidden="true" class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"></path>
</svg>
</div>
</template>
<template x-if="toast.type === 'error'">
<div class="rounded text-error">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M12 9v4" />
<path d="M12 16v.01" />
</svg>
</div>
</template>
<template x-if="toast.type === 'info'">
<div class="rounded text-info">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
<path d="M12 9h.01" />
<path d="M11 12h1v4h1" />
</svg>
</div>
</template>
<template x-if="toast.type === 'warning'">
<div class="rounded text-warning">
<svg aria-hidden="true" class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
</div>
</template>
<span x-text="toast.message" />
</i>
@if ($closeable)
<button @click="toast.dispose()" aria-label="@lang('close')"
class="absolute right-0 p-4 focus:outline-none hover:bg-transparent/10 rounded {{ $alignment->is('bottom') ? 'top-3' : 'top-0' }}">
<svg aria-hidden="true" class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</button>
@endif
</div>
</template>
</div>

View File

@ -1,55 +1,59 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./resources/**/*.blade.php",
"./resources/**/*.js",
"./resources/**/*.vue",
],
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
colors: {
"applications": "#16A34A",
"databases": "#9333EA",
"databases-100": "#9b46ea",
"destinations": "#0284C7",
"sources": "#EA580C",
"services": "#DB2777",
"settings": "#FEE440",
"iam": "#C026D3",
'coollabs': '#6B16ED',
'coollabs-100': '#7317FF',
'coolblack': '#141414',
'coolgray-100': '#181818',
'coolgray-200': '#202020',
'coolgray-300': '#242424',
'coolgray-400': '#282828',
'coolgray-500': '#323232'
}
},
},
variants: {
scrollbar: ['dark'],
extend: {}
},
daisyui: {
themes: [
{
"coollabs": {
"primary": "#6B16ED",
"secondary": "#4338ca",
"accent": "#4338ca",
"neutral": "#1B1D1D",
"base-100": "#212121",
"info": "#2563EB",
"success": "#16A34A",
"warning": "#FCD34D",
"error": "#DC2626",
}
}
content: [
"./resources/**/*.blade.php",
"./resources/**/*.js",
"./resources/**/*.vue",
],
},
plugins: [require('tailwindcss-scrollbar'),require("@tailwindcss/typography"),require("daisyui")],
}
theme: {
extend: {
fontFamily: {
sans: ["Inter", "sans-serif"],
},
colors: {
applications: "#16A34A",
databases: "#9333EA",
"databases-100": "#9b46ea",
destinations: "#0284C7",
sources: "#EA580C",
services: "#DB2777",
settings: "#FEE440",
iam: "#C026D3",
coollabs: "#6B16ED",
"coollabs-100": "#7317FF",
coolblack: "#141414",
"coolgray-100": "#181818",
"coolgray-200": "#202020",
"coolgray-300": "#242424",
"coolgray-400": "#282828",
"coolgray-500": "#323232",
},
},
},
variants: {
scrollbar: ["dark"],
extend: {},
},
daisyui: {
themes: [
{
coollabs: {
primary: "#6B16ED",
secondary: "#4338ca",
accent: "#4338ca",
neutral: "#1B1D1D",
"base-100": "#212121",
info: "#2563EB",
success: "#16A34A",
warning: "#FCD34D",
error: "#DC2626",
},
},
],
},
plugins: [
require("tailwindcss-scrollbar"),
require("@tailwindcss/typography"),
require("daisyui"),
],
};