feat/factory_creation (#1)

Prepping for launch.

Reviewed-on: #1
Co-authored-by: Paul Couture <paul@paulcouture.com>
Co-committed-by: Paul Couture <paul@paulcouture.com>
This commit is contained in:
Paul Couture 2023-12-14 11:33:03 -06:00 committed by Paul Couture
parent 8eb4d14909
commit c4398c641e
342 changed files with 60893 additions and 2557 deletions

4
.env.example Normal file
View File

@ -0,0 +1,4 @@
DB_DATABASE=database_name
DB_USERNAME=database_username
DB_PASSWORD="database_password_for_laravel_app"
DB_ROOT_PASSWORD="database_password_for_superuser"

6
.gitignore vendored
View File

@ -20,6 +20,7 @@ public_html/hot
db/data/
db/dump/
static/dist/
static/*
storage/*.key
@ -28,4 +29,7 @@ Homestead.yaml
Homestead.json
/.vagrant
.phpunit.result.cache
legacypublic
migrated_artworks_files.tar.gz
migrated_thumbnail_files.tar.gz
site/.yarn/releases/yarn-1.22.19.cjs

3
Dockerfile Normal file
View File

@ -0,0 +1,3 @@
FROM shinsenter/laravel:latest
RUN apt update && apt install -y jpegoptim optipng pngquant gifsicle webp libavif-bin

View File

@ -1,7 +1,43 @@
# PodcastArtGenerator
# Podcast Art Generator
Modernizing the No Agenda Art Generator.
Since 2010, the [No Agenda Art Generator](https://noagendaartgenerator.com) has been producing album art for the [No Agenda Podcast](https://noagendashow.net) live via community collaboration by the artists that make up the best podcast art creators in the universe.
In October 2016, the 2.0 release of the Art Generator began based on Laravel 4.2. It has served the podcast and community well, but this project seeks to make it easier for collaborators to contribute, modify, and maintain the art generator while making the product available to more podcasts and artists.
In October 2016, the 2.0 release of the Art Generator began based on Laravel 4.2. It has served the podcast and community well, but this project seeks to make it easier for collaborators to contribute, modify, and maintain the art generator while making the product available to more podcasts and artists.
### License
The Podcast Art Generator is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
### Contibuted Artwork License
Artworks submitted by artists to the Podcast Art Generator are licensed under a Creative Commons License.
By submitting artwork, you are acknowledging you have the right to publish the work and are, by submitting the work, agreeing to place it under the [Creative Commons Attribution-Share Alike 3.0, United States License](http://creativecommons.org/licenses/by-sa/3.0/us/).
#### Copyright
##### Copyright © 2010-2023, Paul Couture, Some Rights Reserved.
---
---
### Built Using:
### Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
### Laravel License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View File

@ -1,22 +1,32 @@
version: '3'
services:
podcastartgenerator-app:
image: shinsenter/laravel:latest
laravel-app:
env_file: .env
build:
context: .
dockerfile: Dockerfile
container_name: ${CONTAINER_NAME:-pcag-laravel}
volumes:
- ./site:/var/www/html
- ./static:/static
- ./nginx/default.conf:/etc/nginx/sites-available/default
- ./nginx/legacy_mappings.conf:/etc/nginx/snippets/legacy_mappings.conf
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
#- ./legacypublic:/legacypublic
environment:
TZ: UTC
PUID: ${UID:-1000}
PGID: ${GID:-1000}
REDIS_HOST: redis
DB_HOST: db
DB_DATABASE: laravel
DB_USERNAME: root
DB_PASSWORD: mydb_p@ssw0rd
# LARAVEL_QUEUE_ENABLED: true
# LARAVEL_QUEUE_OPTIONS: --timeout=60 --tries=3 redis
# LARAVEL_SCHEDULE_ENABLED: true
DB_DATABASE: ${DB_DATABASE}
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
LARAVEL_QUEUE_ENABLED: true
LARAVEL_QUEUE_OPTIONS: --timeout=60 --tries=3 redis
LARAVEL_SCHEDULE_ENABLED: true
PHP_OPEN_BASEDIR: "/var/www/html:/static"
ports:
- "80:80"
links:
@ -26,7 +36,7 @@ services:
static:
image: nginx:alpine
volumes:
- ./static:/usr/share/nginx/html:ro
- ./static:/usr/share/nginx/html
environment:
TZ: UTC
PUID: ${UID:-1000}
@ -35,10 +45,13 @@ services:
- "8181:80"
db:
image: mariadb:latest
env_file: .env
environment:
TZ: UTC
MYSQL_ROOT_PASSWORD: mydb_p@ssw0rd
MYSQL_DATABASE: laravel
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MARIADB_USER: ${DB_USERNAME}
MARIADB_DATABASE: ${DB_DATABASE}
MARIADB_PASSWORD: ${DB_PASSWORD}
volumes:
- "./db/data:/var/lib/mysql"
- "./db/dump:/docker-entrypoint-initdb.d"

38
nginx/default.conf Normal file
View File

@ -0,0 +1,38 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name _;
root /var/www/html/public;
index index.html index.htm index.php;
# SSL
ssl_certificate /etc/ssl/web/server.crt;
ssl_certificate_key /etc/ssl/web/server.key;
# additional config
include extra.d/*.conf;
# health check
location /ping {
access_log off;
include snippets/fastcgi-php.conf;
fastcgi_read_timeout 5s;
fastcgi_pass unix:/var/run/php/php-fpm.sock;
}
# handle .php
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include snippets/fastcgi-php.conf;
}
# index.php fallback
location / {
try_files $uri $uri/ /index.php?$query_string;
}
}

View File

@ -0,0 +1,11 @@
#snippets/legacy_mappings.conf
map $uri $legacy_mapping {
~^/assets/(?<filename>.+)$ $filename;
default $uri;
}
map $filename $modified_filename {
~/(.*) $1---;
default $filename;
}

146
nginx/nginx.conf Normal file
View File

@ -0,0 +1,146 @@
# Generated by nginxconfig.io
# https://www.serverion.com/nginx-config/#?0.domain=_&0.path=%2Fvar%2Fwww%2Fhtml&0.redirect=false&0.force_https=false&0.cert_type=custom&0.ssl_certificate=%2Fetc%2Fssl%2Fweb%2Fserver.crt&0.ssl_certificate_key=%2Fetc%2Fssl%2Fweb%2Fserver.key&0.wordpress&0.proxy_path=%2Fping&0.proxy_pass=unix:%2Fvar%2Frun%2Fphp%2Fphp-fpm.sock&0.index=index.html&content_security_policy=default-src%20'self'%20http:%20https:%20data:%20blob:%20'unsafe-inline';%20frame-ancestors%20'self';&php_server=%2Fvar%2Frun%2Fphp%2Fphp7.3-fpm.sock&expires_media=max&expires_svg=max&expires_fonts=max&user=www-data%20www-data&client_max_body_size=2048&symlink=false
pcre_jit on;
worker_processes auto;
worker_rlimit_nofile 100000;
user www-data www-data;
events {
worker_connections 65535;
use epoll;
multi_accept on;
}
http {
# define common MIME types
include mime.types;
# define the default MIME type
default_type application/octet-stream;
# disable emitting nginx version
server_tokens off;
# disable the directory listing output
autoindex off;
# disable automatic generation of the "ETag"
etag off;
# disable warnings about uninitialized variables are logged
uninitialized_variable_warn off;
# ======================================================================== #
# the bucket size for the maps hash table
map_hash_bucket_size 256;
map_hash_max_size 4096;
# the bucket size for the server names hash tables
server_names_hash_bucket_size 256;
server_names_hash_max_size 4096;
# the bucket size for variables hash tables
variables_hash_max_size 4096;
variables_hash_bucket_size 4096;
# ======================================================================== #
# cache informations about FDs, frequently accessed files
# can boost performance, but you need to test those values
open_file_cache max=200000 inactive=30s;
open_file_cache_valid 30s;
open_file_cache_min_uses 1;
open_file_cache_errors off;
open_log_file_cache max=10 inactive=30s min_uses=1 valid=5m;
# ======================================================================== #
# logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log error;
# to boost I/O on HDD we can disable access logs
log_not_found off;
log_subrequest off;
rewrite_log on;
# copies data between one FD and other from within the kernel
# faster than read() + write()
sendfile off;
sendfile_max_chunk 1m;
# send headers in one piece, it is better than sending them one by one
tcp_nopush on;
# don't buffer data sent, good for small data bursts in real time
tcp_nodelay on;
# large files can be read and sent using multi-threading
# without blocking a worker process
aio threads;
directio 1m;
# how to compare modification time
ssi on;
if_modified_since off;
# set default size of the slice
slice 1m;
# ======================================================================== #
# allow the server to close connection on non responding client,
# this will free up memory
reset_timedout_connection on;
# timeout for reading client request header -- default: 60
client_header_timeout 10s;
# request timed out -- default: 60
client_body_timeout 75s;
# if the request body size is more than the buffer size, then the entire (or partial)
# request body is written into a temporary file
client_body_buffer_size 128k;
# if client stop responding, free up memory -- default: 60
send_timeout 30s;
# server will close connection after this time -- default: 75
keepalive_timeout 30s;
# number of requests client can make over keep-alive
keepalive_requests 100000;
# maximum number and size of buffers
# for large headers to read from client request -- default: 4 8k;
large_client_header_buffers 4 16k;
# ======================================================================== #
# SSL
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# Diffie-Hellman parameter for DHE ciphersuites
ssl_dhparam /etc/ssl/dhparam.pem;
# Mozilla Intermediate configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
# OCSP Stapling
# ssl_stapling on;
# ssl_stapling_verify on;
resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=60s;
resolver_timeout 2s;
# ======================================================================== #
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/snippets/legacy_mappings.conf;
include /etc/nginx/sites-enabled/*;
}

1977
nginx/phpfpm8_2.ini Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
APP_NAME="No Agenda Art Generator"
APP_ENV=local
APP_KEY=base64:H840ho2ltTKV3IC1FQ333AaU8iW2zsn6ma67qhZWerk=
APP_KEY=base64:laravel_key_generated_by_app
APP_DEBUG=true
APP_URL=http://127.0.0.1
@ -9,11 +9,11 @@ LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_HOST=db
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=mydb_p@ssw0rd
DB_DATABASE=database_database_from_docker_env
DB_USERNAME=database_username_from_docker_env
DB_PASSWORD="docker_database_password_from_docker_env"
BROADCAST_DRIVER=log
CACHE_DRIVER=file

5
site/.yarnrc Normal file
View File

@ -0,0 +1,5 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
yarn-path ".yarn/releases/yarn-1.22.19.cjs"

View File

@ -0,0 +1,51 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Artwork;
use App\Models\Episode;
use Illuminate\Support\Facades\DB;
class AddMissingArtworkIdsToEpisodes extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'naart:artwork-to-episodes';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
$oldEpisodes = DB::connection('legacy')->select('SELECT * FROM episodes WHERE artwork_id IS NOT NULL AND published = 1');
foreach($oldEpisodes as $oldEpisode) {
$this->info('Checking old episode ' . $oldEpisode->show_date);
$episode = Episode::where('legacy_id', $oldEpisode->id)->first();
$artwork = Artwork::where('legacy_id', $oldEpisode->artwork_id)->first();
if ($episode && $artwork) {
$this->line('Have artwork ' . $artwork->title . ' for episode ' . $episode->title);
$episode->artwork_id = $artwork->id;
$episode->timestamps = false;
if ($episode->isDirty()) {
$this->info('I need to update this.');
//$episode->save();
} else {
$this->line('No Change Needed.');
}
} else {
$this->error('I am lost.');
}
}
}
}

View File

@ -0,0 +1,125 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Intervention\Image\Facades\Image;
use Illuminate\Support\Facades\Storage;
use Carbon\Carbon;
use App\Models\User;
use App\Models\Artist;
use App\Models\Podcast;
use App\Models\Episode;
use App\Models\Artwork;
use ImageOptimizer;
class GetMissingArtworkMovedCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'naart:wrapup';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
$missingArtworks = DB::connection('legacy')->select('SELECT * FROM artworks WHERE id > 30835 AND approved_by IS NOT NULL');
foreach ($missingArtworks as $missingArtwork) {
$localPath = '/legacypublic' . $missingArtwork->path . '/' . $missingArtwork->filename;
$user = User::where('legacy_id', $missingArtwork->user_id)->with('artists')->first();
$artwork = Artwork::where('legacy_id', $missingArtwork->id)->first();
$episode = Episode::where('legacy_id', $missingArtwork->episode_id)->first();
$approver = User::where('legacy_id', $missingArtwork->approved_by)->with('artists')->first();
if ($artwork) {
$this->line('Artwork ID: ' . $artwork->id);
}
if ($episode) {
$this->line('Episode: ' . $episode->episode_number . ' "' . $episode->title . '"');
}
if (!$artwork) {
$newFilename = $this->uniqueName($episode, $user, $missingArtwork);
$state = [
'title' => $missingArtwork->title,
'description' => '',
'artist_id' => $user->artists->first()->id,
'episode_id' => $episode->id,
'podcast_id' => 1,
'overlay_id' => null,
'filename' => $newFilename . '.jpg',
'created_at' => Carbon::parse($missingArtwork->created_at),
'updated_at' => Carbon::parse($missingArtwork->updated_at),
'legacy_id' => $missingArtwork->id,
'approved_by' => $approver->artists->first()->id,
];
$artwork = Artwork::factory()->state($state)->create();
}
$this->line('Artist: ' . $user->artists->first()->name);
$this->line($localPath);
$filename = 'artworks/' . $artwork->filename;
$thumbnailName = 'thumbnails/' . $artwork->filename;
if (Storage::disk('static')->exists($filename)) {
$this->error($filename . ' already exists. ' . Storage::disk('static')->size($filename));
}
if (Storage::disk('static')->exists($thumbnailName)) {
$this->error($thumbnailName . ' already exists. ' . Storage::disk('static')->size($thumbnailName));
}
$img = Image::make($localPath)
->resize(3000, null, function ($constraint) {
$constraint->aspectRatio();
})
->encode('jpg', 100);
$thumbImg = Image::make($localPath)
->resize(512, null, function ($constraint) {
$constraint->aspectRatio();
})
->encode('jpg', 100);
$imgLocation = Storage::disk('static')->put($filename, $img);
$thumbLocation = Storage::disk('static')->put($thumbnailName, $thumbImg);
$size_before = Storage::disk('static')->size($filename);
$thumb_size_before = Storage::disk('static')->size($thumbnailName);
ImageOptimizer::optimize(Storage::disk('static')->path($filename));
ImageOptimizer::optimize(Storage::disk('static')->path($thumbnailName));
$size_after = Storage::disk('static')->size($filename);
$thumb_size_after = Storage::disk('static')->size($thumbnailName);
$diff = $size_before - $size_after;
$thumbDiff = $thumb_size_before - $thumb_size_after;
$this->line('Filesize before: ' . $size_before);
$this->line('Filesize after: ' . $size_after);
$this->line('Thumb Filesize before: ' . $thumb_size_before);
$this->line('Thumb Filesize after: ' . $thumb_size_after);
}
}
private function checkExistingFilename($name) {
$checkArtwork = Artwork::where('filename', $name . '.jpg')->count();
return $checkArtwork;
}
private function uniqueName($episode, $user, $missingArtwork) {
$i = 0;
$uniqueFilename = $episode->episode_date->format('Y/m/') . Str::slug($user->artists->first()->name . '-' . $missingArtwork->title) . '_' . $missingArtwork->id;
$exists = $this->checkExistingFilename($uniqueFilename);
if (!$exists) {
return $uniqueFilename;
}
while(!$exists) {
$i++;
$uniqueFilename = $uniqueFilename . '_v' . $i;
$exists = $this->checkExistingFilename($uniqueFilename);
}
return $uniqueFilename;
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Artwork;
class LegacyNginxMappingCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'naart:legacy-nginx-mapping';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
$oldLocations = [];
$newLocations = [];
$this->line('# legacy_mappings.conf');
$this->line('');
$this->line('map $uri $new_location {');
$artworks = Artwork::whereNotNull('legacy_filename')->get();
foreach ($artworks as $artwork) {
if (!in_array($artwork->legacy_filename, $oldLocations) && !in_array($artwork->legacy_filename, $newLocations)) {
$oldLocations[] = $artwork->legacy_filename;
$newLocations[] = $artwork->legacy_filename;
$this->line(' "' . $artwork->legacy_filename . '" "/legacy-asset/?legacy_filename=' . urlencode($artwork->legacy_filename) . '";');
}
}
$this->line(' default $uri;');
$this->line('}');
$this->line('');
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use App\Models\Artwork;
use App\Models\Episode;
class MapLegacyArtworkToLegacyEpisodeCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'naart:map-legacy';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Maps legacy artwork to the legacy episode it related to.';
/**
* Execute the console command.
*/
public function handle()
{
$artworks = Artwork::whereNotNull('legacy_id')->get();
foreach ($artworks as $artwork) {
if ($artwork->id > 76200) {
$legacyDetailResponse = $this->getArtworkInfoFromApi($artwork->legacy_id);
$response = $legacyDetailResponse->object();
if ($response->artwork->episode_id) {
$episode = Episode::where('legacy_id', $response->artwork->episode_id)->first();
if ($episode) {
$artwork->episode_id = $episode->id;
$this->line('Artwork ID ' . $artwork->id . ' is mapped to Episode ID ' . $episode->id);
if ($artwork->isDirty()) {
$this->line('This is a new mapping.');
$artwork->save();
}
}
}
}
}
}
private function getArtworkInfoFromApi($artwork_legacy_id) {
$response = Http::timeout(180)
->get('https://noagendaartgenerator.com/artworkapi/' . $artwork_legacy_id,
[
'p' => '7476',
]
);
return $response;
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use App\Models\Artwork;
use App\Models\Episode;
class MapSelectedLegacyArtworkToEpisode extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'naart:map-selected';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Maps the correct artwork selected to the legacy episodes';
/**
* Execute the console command.
*/
public function handle()
{
$episodes = Episode::all();
foreach ($episodes as $episode) {
$this->line('Checking episode ' . $episode->episode_number);
$legacyEpisodeResponse = $this->getEpisodeFromApi($episode->episode_number);
$response = $legacyEpisodeResponse->object();
if ($response->episode->artwork_id && $response->episode->artwork_id + 0 > 0) {
$selectedArtwork = Artwork::where('legacy_id', $response->episode->artwork_id)->first();
if ($selectedArtwork) {
$episode->artwork_id = $selectedArtwork->id;
$this->line('Artwork ID ' . $selectedArtwork->id . ' marked as episode artwork for episode ' . $episode->episode_number);
if ($episode->isDirty()) {
$this->line('This is a new mapping.');
$episode->save();
}
}
}
}
}
private function getEpisodeFromApi($episode_legacy_id) {
$response = Http::timeout(180)
->get('https://noagendaartgenerator.com/episodeapi/' . $episode_legacy_id,
[
'p' => '7476',
]
);
return $response;
}
}

View File

@ -0,0 +1,27 @@
<?php
if (!function_exists('numberSuffix')) {
function numberSuffix($number) {
if (!is_int($number) || $number < 1) {
return $number;
}
$lastDigit = $number % 10;
$secondLastDigit = ($number / 10) % 10;
if ($secondLastDigit == 1) {
return 'th';
}
switch ($lastDigit) {
case 1:
return 'st';
case 2:
return 'nd';
case 3:
return 'rd';
default:
return 'th';
}
}
}

View File

@ -3,6 +3,9 @@
namespace App\Http\Controllers;
use App\Models\Artist;
use App\Models\Artwork;
use App\Models\Podcast;
use App\Models\Episode;
use Illuminate\Http\Request;
class ArtistController extends Controller
@ -14,7 +17,18 @@ class ArtistController extends Controller
*/
public function index()
{
//
$user = auth()->user();
$artists = Artist::whereHas('artworks')
->withCount('artworks')
->orderBy('artworks_count', 'desc')
->paginate(100);
$podcasts = Podcast::where('published', true)->with('episodes')->get();
return view('profile.artists', [
'user' => $user,
'pageTitle' => 'Artists',
'podcasts' => $podcasts,
'artists' => $artists,
]);
}
/**
@ -44,9 +58,23 @@ public function store(Request $request)
* @param \App\Models\Artist $artist
* @return \Illuminate\Http\Response
*/
public function show(Artist $artist)
public function show(Request $request, $slug)
{
//
$user = auth()->user();
$artist = Artist::where('slug', $slug)
->firstOrFail();
$artworks = Artwork::where('artist_id', $artist->id)
->with('episode')
->with('podcast')
->orderBy('artworks.created_at', 'desc')
->paginate($perPage = 92, $columns = ['*'], $pageName = 'artworks');
$podcasts = Podcast::where('published', true)->with('episodes')->get();
return view('profile.artist', [
'user' => $user,
'artist' => $artist,
'artworks' => $artworks,
'podcasts' => $podcasts,
]);
}
/**

View File

@ -3,7 +3,18 @@
namespace App\Http\Controllers;
use App\Models\Artwork;
use App\Models\Podcast;
use App\Models\Episode;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rules\File;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Intervention\Image\Facades\Image;
use ImageOptimizer;
class ArtworkController extends Controller
{
@ -14,7 +25,19 @@ class ArtworkController extends Controller
*/
public function index()
{
//
$user = auth()->user();
$artworks = Artwork::whereNotNull('approved_by')
->with('artist')
->orderBy('episode_id', 'desc')
->orderBy('created_at', 'desc')
->paginate($perPage = 3, $columns = ['*'], $pageName = 'artworks');
$podcasts = Podcast::where('published', true)->with('episodes')->get();
return view('explore.artworks', [
'user' => $user,
'pageTitle' => 'Explore',
'artworks' => $artworks,
'podcasts' => $podcasts,
]);
}
/**
@ -24,7 +47,13 @@ public function index()
*/
public function create()
{
//
$user = auth()->user();
$podcasts = Podcast::where('published', true)->with('episodes')->get();
return view('artworks.submit', [
'user' => $user,
'pageTitle' => 'Submit New Artwork',
'podcasts' => $podcasts,
]);
}
/**
@ -33,20 +62,79 @@ public function create()
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
public function store(Request $request): RedirectResponse
{
//
$validator = Validator::make($request->all(), [
'title' => ['required', 'max:255',],
'podcast' => ['required', 'exists:podcasts,id',],
'description' => ['nullable',],
'file' => ['required', 'image', Rule::dimensions()->ratio(1)->minWidth(512),],
]);
if ($validator->fails()) {
return back()
->withErrors($validator)
->withInput();
}
Log::channel('artwork_import')->info('making new artwork model.');
$podcast = Podcast::where('id', $request->podcast)->with(['episodes' => function($query) {
$query->orderBy('episode_number', 'desc')->limit(1);
}])->first();
$episode = $podcast->episodes->first();
$artist = auth()->user()->artists()->first();
$rawFile = $request->file('file');
$filename = now()->format('Y')
. '/'
. now()->format('m')
. '/'
. Str::slug($artist->name)
. '-'
. Str::slug($request->title)
. '_'
. Str::random(8)
. '.jpg';
$artwork = Artwork::factory()->state([
'title' => $request->title,
'artist_id' => $artist->id,
'description' => $request->description,
'overlay_id' => null,
'podcast_id' => $podcast->id,
'episode_id' => $episode->id,
'filename' => $filename,
])->create();
$img = Image::make($rawFile)->resize(3000, null, function($constraint){
$constraint->aspectRatio();
})
->encode('jpg', 100)
->save(Storage::disk('static')->path('/artworks') . '/' . $artwork->filename);
$thumbImg = Image::make($request->file('file'))->resize(512, null, function($constraint){
$constraint->aspectRatio();
})
->encode('jpg', 100)
->save(Storage::disk('static')->path('/thumbnails') . '/' . $artwork->filename);
ImageOptimizer::optimize(Storage::disk('static')->path('/artworks/' . $artwork->filename));
ImageOptimizer::optimize(Storage::disk('static')->path('/thumbnails/' . $artwork->filename));
return redirect('/artworks/' . $artwork->id);
}
/**
* Display the specified resource.
*
* @param \App\Models\Artwork $artwork
* @param \Illuminate\Http\Request $request
* @param the id of the \App\Models\Artwork $id
* @return \Illuminate\Http\Response
*/
public function show(Artwork $artwork)
public function show(Request $request, $id)
{
//
$user = auth()->user();
$artwork = Artwork::where('id', $id)
->with('podcast')
->with('episode')
->with('artist')
->first();
return view('artworks.artwork', [
'artwork' => $artwork,
'user' => $user,
]);
}
/**
@ -82,4 +170,11 @@ public function destroy(Artwork $artwork)
{
//
}
public function legacyArtLink(Request $request, $any = null)
{
phpinfo();
dd($request->path());
//$artwork = Artwork::where('legacy_filename', '/assets/artwork/')
}
}

View File

@ -4,10 +4,12 @@
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Artist;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
@ -31,17 +33,24 @@ public function create(): View
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'name' => ['unique:artists,name', 'required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'name' => trim($request->name),
'email' => trim(strtolower($request->email)),
'password' => Hash::make($request->password),
]);
$artist = Artist::create([
'user_id' => $user->id,
'name' => trim($request->name),
'slug' => Str::slug(trim($request->name)),
'location' => 'No Agenda Nation',
]);
event(new Registered($user));
Auth::login($user);

View File

@ -2,8 +2,12 @@
namespace App\Http\Controllers;
use App\Models\Episode;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use App\Models\Podcast;
use App\Models\Artworks;
use App\Models\Episode;
class EpisodeController extends Controller
{
@ -44,9 +48,26 @@ public function store(Request $request)
* @param \App\Models\Episode $episode
* @return \Illuminate\Http\Response
*/
public function show(Episode $episode)
public function show(Request $request, $podcast_slug, $slug)
{
//
$user = auth()->user();
$episode = Episode::where('slug', $slug)
->with('artworks')
->with('artwork')
->with('podcast')
->firstOrFail();
$podcasts = Podcast::where('published', true)->with('episodes', function ($query) {
$query->orderBy('episode_number', 'desc');
$query->where('published', true);
$query->take(10);
})->get();
return view('episodes.episode', [
'user' => $user,
'pageTitle' => '"' . $episode->title . '" ' . $episode->podcast->name . ' Episode ' . number_format($episode->episode_number + 0),
'podcast' => $episode->podcast,
'episode' => $episode,
'podcasts' => $podcasts,
]);
}
/**

View File

@ -0,0 +1,144 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use App\Models\Artwork;
use App\Models\Artist;
use App\Models\Episode;
class PageController extends Controller
{
public function landing(Request $request)
{
$user = auth()->user();
$headerCounters = $this->getHeaderCounters();
$recentEpisodes = $this->mostRecentEpisodes();
$recentSubmissions = $this->mostRecentSubmissions();
$leaderboard = $this->leaderboardTwelveMonths();
return view('home.page', [
'user' => $user,
'pageTitle' => 'Home',
'headerCounters' => $headerCounters,
'recentEpisodes' => $recentEpisodes,
'recentSubmissions' => $recentSubmissions,
'leaderboard' => $leaderboard,
'preferredTheme' => $request->session()->get('preferred_theme') ?? 'dark',
]);
}
private function mostRecentSubmissions() {
$artworks = Cache::remember('latestSubmissions', 30, function() {
return Artwork::whereNotNull('approved_by')
->with('artist')
->with('episode')
->with('podcast')
->orderBy('created_at', 'desc')
->limit(50)
->get();
});
return $artworks;
}
private function mostRecentEpisodes()
{
$episodes = Cache::remember('latestEpisodes', 30, function() {
return Episode::where('published', true)
->whereHas('artwork')
->with('podcast')
->with('artwork')
->with('artwork.artist')
->orderBy('episode_date', 'desc')
->limit(10)
->get();
});
return $episodes;
}
private function getHeaderCounters()
{
$headerCounters = [];
$artworkCountNumber = Cache::remember('artworkCountNumber', 10, function() {
return Artwork::all()->count();
});
$artistCountNumber = Cache::remember('artistCountNumber', 10, function() {
return Artist::all()->count();
});
$episodeCountNumber = Cache::remember('episodeCountNumber', 10, function() {
return Episode::all()->count();
});
$headerCounters['Artworks'] = $this->shortNumberCount($artworkCountNumber);
$headerCounters['Artists'] = $this->shortNumberCount($artistCountNumber);
$headerCounters['Episodes'] = $this->shortNumberCount($episodeCountNumber);
return $headerCounters;
}
private function shortNumberCount($number)
{
$units = ['', 'K', 'M', 'B', 'T'];
for ($i = 0; $number >= 1000; $i++) {
$number /= 1000;
}
return [
'number' => $this->numberFormatPrecision($number, 1), //number_format(floatval($number), 1),
'unit' => $units[$i],
];
}
private function numberFormatPrecision($number, $precision = 2, $separator = '.')
{
$numberParts = explode($separator, $number);
$response = $numberParts[0];
if (count($numberParts)>1 && $precision > 0) {
$response .= $separator;
$response .= substr($numberParts[1], 0, $precision);
}
return $response;
}
private function leaderboardTwelveMonths() {
$endDate = now()->endOfDay()->subYear()->format('Y-m-d');
$leaderboard = DB::table('episodes')
->join('artworks', 'artworks.id', '=', 'episodes.artwork_id')
->join('artists', 'artists.id', '=', 'artworks.artist_id')
->select([
DB::raw('artists.id as artistId'),
DB::raw('artists.name as artistName'),
DB::raw('count(artworks.id) as artworkCount')
])
->where('episodes.published', 1)
->where('episodes.episode_date', '>=', $endDate)
->groupBy('artistId')
->orderByDesc('artworkCount')
->limit(10)
->get();
$leaderboard = $this->addArtistModelToLeaderboard($leaderboard);
return $leaderboard;
}
private function addArtistModelToLeaderboard($leaderboard) {
$artistIds = [];
foreach ($leaderboard as $lb) {
$artistIds[] = $lb->artistId;
}
$artists = Artist::whereIn('id', $artistIds)->get();
foreach ($leaderboard as $lb) {
$lb->artist = $artists->where('id', $lb->artistId)->first();
}
$p = 0;
foreach ($leaderboard as $lb) {
$p++;
$lb->position = $p;
}
return $leaderboard;
}
public function setSessionTheme(Request $request) {
}
}

View File

@ -3,8 +3,34 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use App\Models\Podcast;
use App\Models\Artworks;
use App\Models\Episode;
class PodcastController extends Controller
{
//
public function show(Request $request, $slug)
{
$user = auth()->user();
$podcast = Podcast::where('slug', $slug)
->where('published', true)
->firstOrFail();
$episodes = Episode::where('published', true)
->whereNotNull('artwork_id')
->with('artwork')
->with('artworks')
->where('podcast_id', $podcast->id)
->orderBy('episode_number', 'desc')->paginate(100);
$podcasts = Podcast::where('published', true)->with('episodes')->get();
return view('podcasts.podcast', [
'user' => $user,
'pageTitle' => $podcast->name,
'podcast' => $podcast,
'episodes' => $episodes,
'podcasts' => $podcasts,
]);
}
}

View File

@ -39,7 +39,7 @@ class Kernel extends HttpKernel
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],

View File

@ -0,0 +1,68 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\User;
use App\Models\Artist;
use Carbon\Carbon;
use App\Jobs\StashAndOptimizeLegacyArtworkJob;
class ImportLegacyUserJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $user;
/**
* Create a new job instance.
*/
public function __construct($user)
{
$this->user = $user;
}
/**
* Execute the job.
*/
public function handle(): void
{
$name = str_replace(' ', '', $this->user->username);
$email = strtolower(trim($this->user->email));
$email_verified_at = Carbon::parse($this->user->created_at);
$this->createUser($name, $email, $email_verified_at);
}
private function createUser($name, $email, $email_verified_at)
{
$user = User::where('name', $name)->first();
if (!$user) {
$user = User::factory()->state([
'name' => $name,
'email' => $email,
'email_verified_at' => $email_verified_at,
'remember_token' => null,
])->create();
}
$artist = Artist::where('user_id', $user->id)->first();
if (!$artist) {
$artist = Artist::factory()->state([
'user_id' => $user->id,
'name' => $this->user->profile->name,
'avatar' => null,
'header' => null,
'location' => $this->user->profile->location ?? 'No Agenda Art Generator',
'website' => $this->user->profile->website ?? null,
'bio' => null,
])->create();
}
foreach ($this->user->artworks as $artwork) {
StashAndOptimizeLegacyArtworkJob::dispatch($artist, $artwork);
}
}
}

View File

@ -0,0 +1,125 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\User;
use App\Models\Artist;
use App\Models\Artwork;
use App\Models\Episode;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Intervention\Image\Facades\Image;
use Carbon\Carbon;
use ImageOptimizer;
class StashAndOptimizeLegacyArtworkJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $artist;
protected $artwork;
public $tries = 2;
/**
* Create a new job instance.
*/
public function __construct($artist, $artwork)
{
$this->artist = $artist;
$this->artwork = $artwork;
}
/**
* Execute the job.
*/
public function handle(): void
{
$date = Carbon::parse($this->artwork->created_at);
$basename = $date->format('Y')
. '/' . $date->format('m')
. '/' . Str::slug($this->artist->name)
. '-' . Str::slug($this->artwork->title)
. '_' . $this->artwork->id . '.jpg';
$filename = 'artworks/' . $basename;
$thumbnailName = 'thumbnails/' . $basename;
if (Storage::disk('static')->exists($filename)) {
Log::channel('artwork_import')->error($filename . ' already exists. Filesize: ' . Storage::disk('static')->size($filename));
$this->createArtwork($basename);
return;
}
$img = Image::make('/legacypublic' . $this->artwork->path . '/' . $this->artwork->filename)
->resize(3000, null, function ($constraint) {
$constraint->aspectRatio();
})
->encode('jpg', 100);
$thumbImg = Image::make('/legacypublic' . $this->artwork->path . '/' . $this->artwork->filename)
->resize(512, null, function ($constraint) {
$constraint->aspectRatio();
})
->encode('jpg', 100);
$imgLocation = Storage::disk('static')->put($filename, $img);
$thumbLocation = Storage::disk('static')->put($thumbnailName, $thumbImg);
$size_before = Storage::disk('static')->size($filename);
$thumb_size_before = Storage::disk('static')->size($thumbnailName);
ImageOptimizer::optimize(Storage::disk('static')->path($filename));
ImageOptimizer::optimize(Storage::disk('static')->path($thumbnailName));
$size_after = Storage::disk('static')->size($filename);
$thumb_size_after = Storage::disk('static')->size($thumbnailName);
$diff = $size_before - $size_after;
$thumbDiff = $thumb_size_before - $thumb_size_after;
Log::channel('artwork_import')->info('Filesize Before: ' . $size_before);
Log::channel('artwork_import')->info('Filesize After: ' . $size_after);
$perc_smaller = ($diff / $size_before) * 100;
$thumb_perc_smaller = ($thumbDiff / $thumb_size_before) * 100;
Log::channel('artwork_import')->info(number_format($perc_smaller, 2) . '% smaller.');
Log::channel('artwork_import')->info(number_format($thumb_perc_smaller, 2) . '% smaller thumbnail - ' . $thumb_size_after);
Log::channel('artwork_import')->info('Saved and resized ' . $filename);
$this->createArtwork($basename);
}
private function createArtwork($basename) {
$artwork = Artwork::where('legacy_id', $this->artwork->id)->first();
if (!$this->artwork->episode_id) {
$episode = Episode::where('episode_date', '>=', Carbon::parse($this->artwork->created_at)->startOfDay())
->orderBy('episode_date', 'asc')
->first();
} else {
$episode = Episode::where('legacy_id', $this->artwork->episode_id)->first();
}
if (!$artwork) {
$artwork = Artwork::where('filename', $basename)->first();
if ($artwork) {
$artwork->legacy_id = $this->artwork->id;
$artwork->episode_id = $episode->id ?? null;
$artwork->save();
return;
}
}
if (!$artwork) {
Log::channel('artwork_import')->info('making new artwork model for ' . $basename);
Artwork::factory()->state([
'title' => $this->artwork->title,
'artist_id' => $this->artist->id,
'description' => '',
'podcast_id' => 1,
'overlay_id' => $this->artwork->overlay_id,
'episode_id' => $episode->id ?? null,
'filename' => $basename ?? null,
'created_at' => Carbon::parse($this->artwork->created_at),
'updated_at' => Carbon::parse($this->artwork->updated_at),
'legacy_id' => $this->artwork->id,
])->create();
} else {
Log::channel('artwork_import')->info($artwork->id . ' has a model it exists with ' . $artwork->filename);
}
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Livewire;
use Livewire\Component;
class Counter extends Component
{
public $count = 1;
public function increment()
{
$this->count++;
}
public function decrement()
{
$this->count--;
}
public function render()
{
return view('livewire.counter');
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Livewire;
use Livewire\Component;
class Themeswitch extends Component
{
public function light()
{
session()->put('preferred_theme', 'light');
}
public function dark()
{
session()->put('preferred_theme', 'dark');
}
public function render()
{
return view('livewire.themeswitch');
}
}

View File

@ -16,9 +16,29 @@ class Artist extends Model
protected $dates = ['created_at', 'updated_at', 'deleted_at'];
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
protected $fillable = [
'user_id',
'name',
'slug',
'avatar',
'header',
'location',
'website',
'bio',
'created_at',
'updated_at',
'deleted_at',
];
public function user()
{
return $this->belongs_to(User::class);
return $this->belongsTo(User::class);
}
public function artworks()

View File

@ -16,6 +16,12 @@ class Artwork extends Model
protected $dates = ['created_at', 'updated_at', 'deleted_at'];
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
public function podcast()
{
return $this->belongsTo(Podcast::class);

View File

@ -14,6 +14,13 @@ class Episode extends Model
protected $dates = ['episode_date', 'created_at', 'updated_at', 'deleted_at'];
protected $casts = [
'episode_date' => 'date',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
public function podcast()
{
return $this->belongsTo(Podcast::class);
@ -21,7 +28,12 @@ public function podcast()
public function artwork()
{
return $this->hasOne(Artwork::class);
return $this->hasOne(Artwork::class, 'id', 'artwork_id');
}
public function artworks()
{
return $this->hasMany(Artwork::class);
}
public function artist()

View File

@ -16,6 +16,12 @@ class Overlay extends Model
protected $dates = ['created_at', 'updated_at', 'deleted_at'];
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
public function artist()
{
return $this->belongsTo(Artist::class);

View File

@ -16,6 +16,13 @@ class Podcast extends Model
protected $dates = ['created_at', 'updated_at', 'deleted_at', 'added_at'];
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
'added_at' => 'datetime',
];
public function episodes()
{
return $this->hasMany(Episode::class);

View File

@ -14,8 +14,6 @@ class User extends Authenticatable
protected $table = 'users';
protected $dates = ['created_at', 'updated_at'];
/**
* The attributes that are mass assignable.
*
@ -44,6 +42,17 @@ class User extends Authenticatable
*/
protected $casts = [
'email_verified_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* The attributes that should be appended.
*
* @var array<string, string>
*/
protected $appends = [
];
public function artists()

View File

@ -13,6 +13,11 @@ class Wallet extends Model
protected $dates = ['created_at', 'updated_at'];
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
public function walletType()
{
return $this->hasOne(WalletType::class);

View File

@ -13,6 +13,11 @@ class WalletType extends Model
protected $dates = ['created_at', 'updated_at'];
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
public function wallets()
{
return $this->hasMany(Wallet::class);

View File

@ -3,6 +3,9 @@
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\View;
use App\Models\Podcast;
class AppServiceProvider extends ServiceProvider
{
@ -19,6 +22,8 @@ public function register(): void
*/
public function boot(): void
{
//
Paginator::useBootstrapFive();
$publishedPodcasts = Podcast::where('published', true)->select(['name', 'slug'])->get();
View::share(['navPodcasts' => $publishedPodcasts]);
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Providers\Filament;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Pages;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Filament\Widgets;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->login()
->colors([
'primary' => Color::Amber,
])
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([
Pages\Dashboard::class,
])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([
Widgets\AccountWidget::class,
Widgets\FilamentInfoWidget::class,
])
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
]);
}
}

View File

@ -17,7 +17,7 @@ class RouteServiceProvider extends ServiceProvider
*
* @var string
*/
public const HOME = '/dashboard';
public const HOME = '/';
/**
* Define your route model bindings, pattern filters, and other route configuration.

View File

@ -2,16 +2,24 @@
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"],
"keywords": [
"laravel",
"framework"
],
"license": "MIT",
"require": {
"php": "^8.1",
"andreiio/blade-remix-icon": "^2.6",
"blade-ui-kit/blade-icons": "^1.5",
"filament/filament": "^3.0-stable",
"guzzlehttp/guzzle": "^7.2",
"intervention/image": "^2.7",
"laravel/framework": "^10.10",
"laravel/sanctum": "^3.2",
"laravel/sanctum": "^3.3",
"laravel/tinker": "^2.8",
"livewire/livewire": "^2.12"
"livewire/livewire": "^3.2",
"mckenziearts/blade-untitledui-icons": "^1.2",
"spatie/laravel-image-optimizer": "^1.7"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",
@ -28,7 +36,10 @@
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"files": [
"app/Helpers/pcagHelpers.php"
]
},
"autoload-dev": {
"psr-4": {
@ -38,7 +49,8 @@
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
"@php artisan package:discover --ansi",
"@php artisan filament:upgrade"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"

3361
site/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -59,6 +59,8 @@
'asset_url' => env('ASSET_URL'),
'static_asset_url' => env('STATIC_ASSET_URL'),
/*
|--------------------------------------------------------------------------
| Application Timezone
@ -167,6 +169,7 @@
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class,
App\Providers\RouteServiceProvider::class,
])->toArray(),

183
site/config/blade-icons.php Normal file
View File

@ -0,0 +1,183 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Icons Sets
|--------------------------------------------------------------------------
|
| With this config option you can define a couple of
| default icon sets. Provide a key name for your icon
| set and a combination from the options below.
|
*/
'sets' => [
// 'default' => [
//
// /*
// |-----------------------------------------------------------------
// | Icons Path
// |-----------------------------------------------------------------
// |
// | Provide the relative path from your app root to your SVG icons
// | directory. Icons are loaded recursively so there's no need to
// | list every sub-directory.
// |
// | Relative to the disk root when the disk option is set.
// |
// */
//
// 'path' => 'resources/svg',
//
// /*
// |-----------------------------------------------------------------
// | Filesystem Disk
// |-----------------------------------------------------------------
// |
// | Optionally, provide a specific filesystem disk to read
// | icons from. When defining a disk, the "path" option
// | starts relatively from the disk root.
// |
// */
//
// 'disk' => '',
//
// /*
// |-----------------------------------------------------------------
// | Default Prefix
// |-----------------------------------------------------------------
// |
// | This config option allows you to define a default prefix for
// | your icons. The dash separator will be applied automatically
// | to every icon name. It's required and needs to be unique.
// |
// */
//
// 'prefix' => 'icon',
//
// /*
// |-----------------------------------------------------------------
// | Fallback Icon
// |-----------------------------------------------------------------
// |
// | This config option allows you to define a fallback
// | icon when an icon in this set cannot be found.
// |
// */
//
// 'fallback' => '',
//
// /*
// |-----------------------------------------------------------------
// | Default Set Classes
// |-----------------------------------------------------------------
// |
// | This config option allows you to define some classes which
// | will be applied by default to all icons within this set.
// |
// */
//
// 'class' => '',
//
// /*
// |-----------------------------------------------------------------
// | Default Set Attributes
// |-----------------------------------------------------------------
// |
// | This config option allows you to define some attributes which
// | will be applied by default to all icons within this set.
// |
// */
//
// 'attributes' => [
// // 'width' => 50,
// // 'height' => 50,
// ],
//
// ],
],
/*
|--------------------------------------------------------------------------
| Global Default Classes
|--------------------------------------------------------------------------
|
| This config option allows you to define some classes which
| will be applied by default to all icons.
|
*/
'class' => '',
/*
|--------------------------------------------------------------------------
| Global Default Attributes
|--------------------------------------------------------------------------
|
| This config option allows you to define some attributes which
| will be applied by default to all icons.
|
*/
'attributes' => [
// 'width' => 50,
// 'height' => 50,
],
/*
|--------------------------------------------------------------------------
| Global Fallback Icon
|--------------------------------------------------------------------------
|
| This config option allows you to define a global fallback
| icon when an icon in any set cannot be found. It can
| reference any icon from any configured set.
|
*/
'fallback' => '',
/*
|--------------------------------------------------------------------------
| Components
|--------------------------------------------------------------------------
|
| These config options allow you to define some
| settings related to Blade Components.
|
*/
'components' => [
/*
|----------------------------------------------------------------------
| Disable Components
|----------------------------------------------------------------------
|
| This config option allows you to disable Blade components
| completely. It's useful to avoid performance problems
| when working with large icon libraries.
|
*/
'disabled' => false,
/*
|----------------------------------------------------------------------
| Default Icon Component Name
|----------------------------------------------------------------------
|
| This config option allows you to define the name
| for the default Icon class component.
|
*/
'default' => 'icon',
],
];

View File

@ -56,6 +56,26 @@
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => false,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'legacy' => [
'driver' => 'mysql',
'url' => env('LEGACY_DATABASE_URL'),
'host' => env('LEGACY_DB_HOST', '127.0.0.1'),
'port' => env('LEGACY_DB_PORT', '3306'),
'database' => env('LEGACY_DB_DATABASE', 'forge'),
'username' => env('LEGACY_DB_USERNAME', 'forge'),
'password' => env('LEGACY_DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
@ -148,4 +168,4 @@
],
];
];

View File

@ -0,0 +1,66 @@
<?php
use Spatie\ImageOptimizer\Optimizers\Cwebp;
use Spatie\ImageOptimizer\Optimizers\Gifsicle;
use Spatie\ImageOptimizer\Optimizers\Jpegoptim;
use Spatie\ImageOptimizer\Optimizers\Optipng;
use Spatie\ImageOptimizer\Optimizers\Pngquant;
use Spatie\ImageOptimizer\Optimizers\Svgo;
return [
/*
* When calling `optimize` the package will automatically determine which optimizers
* should run for the given image.
*/
'optimizers' => [
Jpegoptim::class => [
'-m72', // set maximum quality to 85%
'--strip-all', // this strips out all text information such as comments and EXIF data
'--all-progressive', // this will make sure the resulting image is a progressive one
],
Pngquant::class => [
'--force', // required parameter for this package
],
Optipng::class => [
'-i0', // this will result in a non-interlaced, progressive scanned image
'-o2', // this set the optimization level to two (multiple IDAT compression trials)
'-quiet', // required parameter for this package
],
Svgo::class => [
'--disable=cleanupIDs', // disabling because it is know to cause troubles
],
Gifsicle::class => [
'-b', // required parameter for this package
'-O3', // this produces the slowest but best results
],
Cwebp::class => [
'-m 6', // for the slowest compression method in order to get the best compression.
'-pass 10', // for maximizing the amount of analysis pass.
'-mt', // multithreading for some speed improvements.
'-q 90', // quality factor that brings the least noticeable changes.
],
],
/*
* The directory where your binaries are stored.
* Only use this when you binaries are not accessible in the global environment.
*/
'binary_path' => '',
/*
* The maximum time in seconds each optimizer is allowed to run separately.
*/
'timeout' => 60,
/*
* If set to `true` all output of the optimizer binaries will be appended to the default log.
* You can also set this to a class that implements `Psr\Log\LoggerInterface`.
*/
'log_optimizer_activity' => false,
];

View File

@ -65,6 +65,13 @@
'replace_placeholders' => true,
],
'artwork_import' => [
'driver' => 'single',
'path' => storage_path('logs/artwork_import.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),

View File

@ -2,10 +2,20 @@
namespace Database\Factories;
use App\Models\Artist;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Str;
class ArtistFactory extends Factory
{
/**
* The name of the factory's corresponding model
*
* @var string
*/
protected $model = Artist::class;
/**
* Define the model's default state.
*
@ -13,8 +23,17 @@ class ArtistFactory extends Factory
*/
public function definition()
{
$name = fake()->name();
$slug = Str::slug(strtolower(trim($name)));
return [
//
'user_id' => User::factory(),
'name' => $name,
'slug' => $slug,
'avatar' => fake()->imageUrl(512, 512),
'header' => fake()->imageUrl(270, 185),
'location' => fake()->city() . ', ' . fake()->state(),
'website' => rand(0, 1) ? fake()->url : null,
'bio' => fake()->paragraphs(rand(1, 3), true),
];
}
}

View File

@ -3,9 +3,23 @@
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
use App\Models\Artist;
use App\Models\Artwork;
use App\Models\Episode;
use App\Models\Podcast;
use App\Models\Overlay;
class ArtworkFactory extends Factory
{
/**
* The name of the factory's corresponding model
*
* @var string
*/
protected $model = Artwork::class;
/**
* Define the model's default state.
*
@ -13,8 +27,19 @@ class ArtworkFactory extends Factory
*/
public function definition()
{
$created = fake()->dateTimeThisDecade();
return [
//
'title' => fake()->name(),
'description' => rand(0, 1) ? fake()->paragraphs(rand(1, 3), true) : null,
'artist_id' => Artist::factory(),
'podcast_id' => Podcast::factory(),
'episode_id' => Episode::factory(),
'overlay_id' => rand(0, 1) ? Overlay::factory() : null,
'filename' => fake()->imageUrl(3000, 3000),
'created_at' => $created,
'updated_at' => $created,
'legacy_id' => null,
'approved_by' => null,
];
}
}

View File

@ -2,10 +2,24 @@
namespace Database\Factories;
use App\Models\Artist;
use App\Models\Artwork;
use App\Models\Episode;
use App\Models\Podcast;
use App\Models\Overlay;
use Illuminate\Database\Eloquent\Factories\Factory;
use Carbon\Carbon;
use Illuminate\Support\Str;
class EpisodeFactory extends Factory
{
/**
* The name of the factory's corresponding model
*
* @var string
*/
protected $model = Episode::class;
/**
* Define the model's default state.
*
@ -13,8 +27,20 @@ class EpisodeFactory extends Factory
*/
public function definition()
{
$title = fake()->name();
$slug = Str::slug($title);
$created = fake()->dateTimeThisDecade();
return [
//
'podcast_id' => Podcast::factory(),
'artwork_id' => Artwork::factory(),
'published' => fake()->boolean(),
'episode_date' => fake()->dateTimeThisDecade(),
'slug' => $slug,
'title' => $title,
'mp3' => fake()->url(),
'created_at' => $created,
'updated_at' => $created,
'legacy_id' => null,
];
}
}

View File

@ -3,9 +3,24 @@
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
use App\Models\Artist;
use App\Models\Artwork;
use App\Models\Episode;
use App\Models\Podcast;
use App\Models\Overlay;
class OverlayFactory extends Factory
{
/**
* The name of the factory's corresponding model
*
* @var string
*/
protected $model = Overlay::class;
/**
* Define the model's default state.
*
@ -13,8 +28,14 @@ class OverlayFactory extends Factory
*/
public function definition()
{
$name = fake()->name();
$slug = Str::slug($name);
return [
//
'name' => $name,
'artist_id' => Artist::factory(),
'podcast_id' => Podcast::factory(),
'available' => fake()->boolean(),
'filename' => fake()->imageUrl(3000, 3000),
];
}
}

View File

@ -4,9 +4,21 @@
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
use App\Models\Artist;
use App\Models\Artwork;
use App\Models\Episode;
use App\Models\Podcast;
use App\Models\Overlay;
class PodcastFactory extends Factory
{
/**
* The name of the factory's corresponding model
*
* @var string
*/
protected $model = Podcast::class;
/**
* Define the model's default state.
*
@ -14,12 +26,14 @@ class PodcastFactory extends Factory
*/
public function definition()
{
$name = fake()->name();
$slug = Str::slug($name);
return [
'name' => fake()->name(),
'name' => $name,
'description' => fake()->paragraphs(rand(1,3), true),
'website' => 'https://' . fake()->domainName(),
'feed' => fake()->url(),
'slug' => fake()->slug(),
'feed' => 'podcast/' . $slug,
'slug' => $slug,
'published' => fake()->boolean(),
'added_at' => fake()->dateTimeThisDecade(),
];

View File

@ -4,9 +4,23 @@
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Hash;
use App\Models\Artist;
use App\Models\Artwork;
use App\Models\Episode;
use App\Models\Podcast;
use App\Models\Overlay;
use App\Models\User;
class UserFactory extends Factory
{
/**
* The name of the factory's corresponding model
*
* @var string
*/
protected $model = User::class;
/**
* Define the model's default state.
*
@ -14,12 +28,14 @@ class UserFactory extends Factory
*/
public function definition()
{
$password = Hash::make(Str::random(rand(8, 16)));
return [
'name' => $this->faker->name(),
'email' => $this->faker->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'password' => $password,
'remember_token' => Str::random(10),
'legacy_id' => null,
];
}

View File

@ -3,6 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use App\Models\User;
class CreateArtistsTable extends Migration
{
@ -15,7 +16,10 @@ public function up()
{
Schema::create('artists', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->foreignIdFor(User::class)
->constrained()
->cascadeOnUpdate()
->cascadeOnDelete();
$table->string('name')->unique();
$table->string('avatar')->nullable();
$table->string('header')->nullable();

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('episodes', function (Blueprint $table) {
$table->decimal('episode_number', 8, 1)->after('episode_date')->default(0);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('episodes', function (Blueprint $table) {
$table->dropColumn('episode_number');
});
}
};

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('artworks', function (Blueprint $table) {
$table->bigInteger('legacy_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('artworks', function (Blueprint $table) {
$table->dropColumn('legacy_id');
});
}
};

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('episodes', function (Blueprint $table) {
$table->bigInteger('legacy_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('episodes', function (Blueprint $table) {
$table->dropColumn('legacy_id');
});
}
};

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('artworks', function (Blueprint $table) {
$table->bigInteger('approved_by')->nullable()->after('filename');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('artworks', function (Blueprint $table) {
$table->dropColumn('approved_by');
});
}
};

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('users', function (Blueprint $table) {
$table->integer('legacy_id')->nullable()->unsigned();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('legacy_id');
});
}
};

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('artworks', function (Blueprint $table) {
$table->string('legacy_filename')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('artworks', function (Blueprint $table) {
$table->dropColumn('legacy_filename');
});
}
};

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('users', function (Blueprint $table) {
$table->string('theme')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('theme');
});
}
};

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('artists', function (Blueprint $table) {
$table->string('slug')->after('name');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('artists', function (Blueprint $table) {
$table->dropColumn('slug');
});
}
};

View File

@ -0,0 +1,60 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Episode;
use App\Models\Podcast;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Carbon\Carbon;
class EpisodeSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$current_page = 1;
$response = $this->getResponseFromApi($current_page);
$last_page = $response->object()->last_page;
$this->command->info('Last Page: ' . $last_page);
$podcast = Podcast::find(1);
while ($current_page <= $last_page) {
$this->command->info('Getting Page ' . $current_page);
foreach ($response->object()->data as $episode) {
$podcastEpisode = Episode::where('title', $episode->title)->first();
if (!$podcastEpisode) {
$podcastEpisode = Episode::factory()->state([
'podcast_id' => 1,
'episode_date' => Carbon::parse($episode->show_date),
'published' => (bool)$episode->published,
'artwork_id' => null,
'slug' => $episode->episode_number . '_' . Str::slug($episode->title),
'title' => $episode->title,
'mp3' => $episode->link,
'created_at' => Carbon::parse($episode->created_at),
'updated_at' => Carbon::parse($episode->updated_at),
'legacy_id' => $episode->id ?? null
])->create();
} else {
$podcastEpisode->legacy_id = $episode->id ?? null;
if ($podcastEpisode->isDirty()) {
$podcastEpisode->save();
}
}
$this->command->info('Created ' . $episode->show_date . ' - (' . $episode->episode_number . ') ' . $episode->title);
}
$current_page++;
$response = $this->getResponseFromApi($current_page);
}
}
private function getResponseFromApi($current_page) {
$response = Http::timeout(180)
->get('https://noagendaartgenerator.com/episodesjson?p=7476&page=' . $current_page);
return $response;
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Episode;
use App\Models\Podcast;
use App\Models\Artwork;
use Illuminate\Support\Str;
use Illuminate\Support\Facade\Log;
class FixLegacyEpisodeSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$episodes = Episode::all();
foreach ($episodes as $episode) {
if (is_null($episode->episode_number) || $episode->episode_number == 0) {
$ep_num_arr = explode('_', $episode->slug);
$episode->episode_number = $ep_num_arr[0];
}
if ($episode->isDirty()) {
$episode->save();
}
}
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class MapLegacyIdsSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@ -115,7 +115,31 @@ public function run()
$feed = 'https://hogstory.net/feed/podcast';
$added = '2019-02-22 20:00:00';
$this->createPodcast($name, $description, $website, $slug, $feed, $added);
$name = 'Rare Encounter';
$description = 'AbleKirby and coldacid converse on anime they watch, books and manga they read, games they play,
and all the tech stuff that they come across.';
$website = 'http://rareencounter.net';
$slug = 'rare-encounter';
$feed = 'https://rareencounter.net/external.php?name=RSS';
$added = '2020-07-16 20:00:00';
$this->createPodcast($name, $description, $website, $slug, $feed, $added);
$name = 'Unrelenting';
$description = 'The Unrelenting Podcast is hosted by Gene Naftulvev and Darren ONeill. It covers politics, technology, pop-culture, and more!';
$website = 'http://unrelenting.show';
$slug = 'unrelenting';
$feed = 'https://www.unrelenting.show/feed/podcast/';
$added = '2021-10-29 20:00:00';
$this->createPodcast($name, $description, $website, $slug, $feed, $added);
$name = 'The Boostagram Ball';
$description = 'The First Podcast with Value4Value Music hosted by Adam Curry';
$website = 'https://boostagramball.com';
$slug = 'boostagram-ball';
$feed = 'https://mp3s.nashownotes.com/bballrss.xml';
$added = '2023-07-29 20:00:00';
$this->createPodcast($name, $description, $website, $slug, $feed, $added);
}
private function createPodcast($name, $description, $website, $slug, $feed, $added) {

View File

@ -0,0 +1,105 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;
use App\Models\Artist;
use App\Models\Artwork;
use App\Models\Episode;
use App\Models\Podcast;
use App\Models\Overlay;
use App\Jobs\StashAndOptimizeLegacyArtworkJob;
use App\Jobs\ImportLegacyUserJob;
use Carbon\Carbon;
class SeedFromOldApiSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//$this->populateLegacyArtworks();
//die();
$current_page = 0;
$totalArtworks = 0;
$missingArtworks = 0;
$missingModel = 0;
$response = $this->getResponseFromApi($current_page);
$last_page = $response->object()->users->last_page;
while ($current_page <= $last_page) {
$this->command->info('Getting Page ' . $current_page);
foreach ($response->object()->users->data as $user) {
$this->command->line('Getting Art for ' . $user->profile->name . ', found ' . count($user->artworks) . ' artworks.');
$totalArtworks += count($user->artworks);
$legacyUser = Artist::where('name', $user->profile->name)->first();
if (!$legacyUser) {
ImportLegacyUserJob::dispatch($user);
} else {
if ($legacyUser->artworks->count() < count($user->artworks)) {
$countDiff = count($user->artworks) - $legacyUser->artworks->count();
$missingArtworks += $countDiff;
$this->command->comment('Artist ID '
. $legacyUser->id
. ' '
. $legacyUser->name
. ' has '
. $legacyUser->artworks->count()
. ' artworks.');
$this->command->error('Missing ' . $countDiff . ' artworks.');
foreach ($user->artworks as $artwork) {
$date = Carbon::parse($artwork->created_at);
$basename = $date->format('Y')
. '/' . $date->format('m')
. '/' . Str::slug($legacyUser->name)
. '-' . Str::slug($artwork->title)
. '_' . $artwork->id . '.jpg';
if (Storage::disk('static')->exists('artworks/' . $basename)) {
$artworkModel = Artwork::where('filename', $basename)->first();
if (!$artworkModel) {
$missingModel++;
$this->command->error($basename . ' exists.');
StashAndOptimizeLegacyArtworkJob::dispatch($legacyUser, $artwork);
}
}
}
} else {
$this->command->line('Locally stored all of ' . $legacyUser->name . '\'s artworks.');
}
}
}
$current_page++;
$response = $this->getResponseFromApi($current_page);
}
$this->command->info('Total Artworks: ' . $totalArtworks);
$this->command->info('Total Missing: ' . $missingArtworks);
$this->command->info('Total Missing Model: ' . $missingModel);
}
private function getResponseFromApi($current_page) {
$response = Http::timeout(180)
->get('https://noagendaartgenerator.com/artistapi',
[
'p' => '7476',
'page' => $current_page,
]
);
return $response;
}
private function populateLegacyArtworks() {
$artworks = Artwork::whereNull('legacy_id')->get();
foreach ($artworks as $artwork) {
$file = pathinfo($artwork->filename);
$filename = explode('_', $file['filename']);
$legacy_id = end($filename);
$artwork->legacy_id = $legacy_id;
$artwork->save();
}
}
}

1779
site/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,20 @@
"axios": "^1.1.2",
"laravel-vite-plugin": "^0.7.5",
"postcss": "^8.4.6",
"sass": "^1.63.6",
"tailwindcss": "^3.1.0",
"vite": "^4.0.0"
"vite": "^4.0.0",
"wolfy87-eventemitter": "4.2.0"
},
"version": "0.0.0",
"dependencies": {
"aos": "^3.0.0-beta.6",
"guillotine": "^1.3.1",
"isotope-layout": "^3.0.6",
"jquery": "^3.7.0",
"jquery-nice-select": "^1.1.0",
"js.cookie": "^0.0.4",
"slick-carousel": "^1.8.1",
"waypoints": "^4.0.1"
}
}

990
site/public/adm/adminer.css Normal file
View File

@ -0,0 +1,990 @@
/** theme "easy on the eyes" for Adminer by p.galkaev@miraidenshi-tech.jp */
@import url(//fonts.googleapis.com/css?family=Source+Sans+Pro:400,900);
/* reset
----------------------------------------------------------------------- */
*,
*:after,
*:before {
margin: 0;
padding: 0;
outline: none;
cursor: default;
-webkit-appearance: none;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-webkit-print-color-adjust: exact;
}
/* for font awesome */
*:not(.fa) {
font-family: 'Source Sans Pro', sans-serif;
}
#logins a, #tables a, #tables span {
background: none;
}
p,
form
{
margin: 0;
margin-bottom: 20px;
font-size: 14px;
}
p:last-child,
form:last-child
{
margin-bottom: 0;
}
.type,
.options select
{
width: 100%;
}
sup{
display: none;
}
/* js tooltip
----------------------------------------------------------------------- */
.js .column {
position: absolute;
padding: 0;
margin-top: 0;
top: 50px;
z-index: 9;
left: 0px;
width: 100%;
}
.js .column:not(.hidden){
display: flex;
}
.js .column a{
text-align: center;
color: black;
font-weight: bold;
flex-grow: 1;
background: #fb4;
height: 40px;
line-height: 40px;
font-size: 15px;
font-weight: normal;
}
.js .column a:hover{
background-color: gold;
color: black;
}
#help {
position: absolute;
border: none;
background: #fb4;
font-family: monospace;
z-index: 1;
font-size: 14px;
line-height: 30px;
padding: 0;
}
#help a{
color: black;
height: 100%;
display: block;
padding: 0 10px;
}
#help a:hover{
background-color: gold;
}
#help, .js .column{
display: none;
}
/* error and message
----------------------------------------------------------------------- */
.error, .message {
padding: 5px 15px 7px;
margin: 10px 0;
font-size: 14px;
display: table;
border-radius: 3px;
color: white;
}
.error{
background-color: crimson;
}
.message{
background-color: seagreen;
}
/* scroll bar
----------------------------------------------------------------------- */
::selection {
background-color: #2a65ae;
}
/*
::-moz-selection {
background-color: #333;
}*/
/* scroll bar
----------------------------------------------------------------------- */
::-webkit-scrollbar {
background-color: black;
cursor: pointer;
}
::-webkit-scrollbar-thumb {
background-color: #555;
cursor: pointer;
}
::-webkit-scrollbar:vertical{
width: 6px;
}
::-webkit-scrollbar-thumb:vertical{
border-left: 0px solid black;
width: 6px;
}
::-webkit-scrollbar:horizontal{
height: 6px;
}
::-webkit-scrollbar-thumb:horizontal{
border-top: 0px solid black;
height: 6px;
}
::-webkit-scrollbar-corner{
color: black;
background-color: black;
border-color: black;
}
::-webkit-resizer{
background-color: #555;
border-radius: 100%;
}
/* html and body
----------------------------------------------------------------------- */
html,
body {
width: 100%;
height: 100%;
max-height: 100%;
overflow: hidden;
}
body{
min-height: 100%;
font-size: 14px;
position: relative;
color: #ccc;
background-color: black;
overflow: hidden;
display: flex;
flex-wrap: nowrap;
font: inherit;
}
/* headings
----------------------------------------------------------------------- */
h1{
font-size: 24px;
margin: 0;
padding: 0 18px;
border-bottom: 1px solid #444;
font-weight: bold;
height: 70px;
line-height: 70px;
color: #555;
background: none;
}
h2{
font-size: 24px;
margin: 0;
padding: 0;
padding-left: 50px;
border-bottom: 1px solid #333;
color: #2CC990;
font-weight: bold;
background: none;
height: 70px;
line-height: 70px;
text-transform: uppercase;
}
h3{
font-weight: bold;
font-size: 24px;
margin: 40px 0 10px;
color: #2CC990;
padding-bottom: 5px;
}
/* links
----------------------------------------------------------------------- */
a{
color: inherit;
cursor: pointer;
}
a:hover, a:visited{
color: inherit;
}
a:link:hover, a:visited:hover {
color: inherit;
text-decoration: none;
}
/* table
----------------------------------------------------------------------- */
table{
margin: 0;
margin-bottom: 20px;
border: 0;
border-collapse: collapse;
font-size: 13px;
width: 100%;
/*table-layout: fixed;*/
}
tr:hover th,
.checked th
{
background: #333 !important;
color: #ddd;
border-color: none;
}
tr:hover td,
.checked td
{
background: #222 !important;
color: #ddd;
border-color: none;
}
.links + table tr:hover th{
color: #ddd;
background: #336f5a !important;
}
.links + table tr:hover td{
background: #2CC990 !important;
color: #333;
}
p + table{
margin-top: 20px;
}
tr{
padding-bottom: 1px;
}
td, th {
border: 0;
border-right: 1px solid #333;
padding: 0 12px;
line-height: 30px;
position: relative;
}
td:last-child,
th:last-child{
border-right: none;
}
th{
position: relative;
background: #222;
font-weight: normal;
width: 17%;
border-left: 5px solid #336f5a;
border-bottom: 1px solid rgba(255, 255, 255, .13);
color: #999;
}
.checkable td:first-child{
background: #222;
border-right-style: solid;
}
table.checkable th{
border-left: none;
}
td{
background: #000;
border-bottom: 1px solid rgba(255, 255, 255, .1);
}
.odd th{
background: #222;
}
.odd td{
background: #000;
}
thead td,
thead th
{
background: transparent !important;
color: #ccc;
border-right-style: dashed;
font-weight: bold;
}
table#edit-fields td,
table#edit-fields th
{
padding: 0;
padding-left: 5px;
}
table#edit-fields thead th,
table#edit-fields thead td
{
padding-left: 10px;
}
thead tr:hover th,
thead tr:hover td,
.links + table thead tr:hover th,
.links + table thead tr:hover td,
table#edit-fields thead tr:hover th,
table#edit-fields thead tr:hover td
{
background-color: transparent !important;
color: inherit !important;
border-bottom: 1px solid rgba(255, 255, 255, .1) !important;
}
thead tr:hover th{
border-bottom: 1px solid rgba(255, 255, 255, .13) !important;
}
thead th {
border-left-color: transparent;
text-align: left;
padding: 10px;
}
/* form
----------------------------------------------------------------------- */
input,
select,
textarea
{
color: #333;
font-size: 15px;
height: 30px;
background-color: #ddd;
border: none;
border-radius: 3px;
line-height: 28px;
cursor: pointer;
padding: 0;
padding-left: 10px;
-webkit-appearance: none;
outline: none;
}
input:hover,
select:hover,
input:focus,
select:focus
{
background-color: #bbb;
}
th input,
td input,
th select,
td select,
td textarea
{
background-color: transparent;
color: pink;
width: 100%;
display: inline;
border-left: 1px dashed #555;
border-radius: 0;
}
th input:hover,
th select:hover,
td input:hover,
td select:hover,
th input:focus,
th select:focus,
td input:focus,
td select:focus
{
background-color: rgba(255, 255, 255, .15);
}
th input[type='checkbox'],
th input[type='radio'],
td input[type='checkbox'],
td input[type='radio']{
border-left: none;
background-color: transparent !important;
}
td input + a,
td input + a:visited
{
text-transform: uppercase;
margin-left: 5px;
color: dodgerblue;
font-size: 12px;
font-weight: normal;
}
td input + a:hover{
color: lightskyblue !important;
}
input.icon{
padding-left: 0;
}
input.icon::after{
content: '';
}
th select,
td select
{
color: lightcoral;
}
input[type='number'] {
min-width: 55px;
}
/* radio */
input[type='radio']{
-webkit-appearance: radio;
width: 18px;
height: 18px;
vertical-align: middle;
margin-left: 8px;
margin-right: 0;
}
/* checkbox */
input[type='checkbox']{
width: 30px;
height: 30px;
margin-right: 6px;
position: relative;
border-radius: 2px;
margin-left: 20px;
}
input[type=checkbox]:hover{
border-color: white;
}
input[type=checkbox]::after {
cursor: pointer;
position: absolute;
content: '×';
left: 17%;
top: 4.5%;
color: #ccc;
font-size: 35px;
font-family: sans-serif;
font-weight: bold;
}
input[type=checkbox]:hover::after {
color: #aaa;
}
input[type=checkbox]:checked::after {
color: #333;
}
td input[type='checkbox'],
th input[type='checkbox']
{
margin-left: 10px;
margin-right: 26px;
}
td input[type='checkbox']::after{
left: 10%;
top: -2px;
color: #333;
}
td input[type='checkbox']:hover::after{
color: #555;
}
td input[type='checkbox']:checked::after{
color: #ddd;
}
p input:first-child{
margin-left: 8px;
}
label{
line-height: 27px;
font-size: 14px;
}
th label{
line-height: 35px;
}
label input {
vertical-align: top;
}
/* submit */
input[type='submit']{
color: white;
background-color: royalblue;
padding: 0 25px;
margin-right: 20px;
border-radius: 2px;
}
input[type='submit']:hover{
background-color: #214ac5;
}
/* select */
select{
padding-left: 6px;
}
/* textarea */
textarea{
min-height: 150px;
width: 100%;
}
/* fieldset */
fieldset {
display: inline;
vertical-align: top;
padding: 4px 7px 7px;
margin: 0 5px 10px;
border: 1px dashed #555;
border-radius: 2px;
min-height: 60px;
}
fieldset > div{
display: flex;
}
fieldset > div * + p{
margin-left: 10px;
}
fieldset > div > div{
margin-left: 10px;
}
fieldset > div > div:first-child{
margin-left: 0;
}
fieldset > div input,
fieldset > div select
{
margin-right: 5px;
}
fieldset > div input[type='checkbox']{
margin-left: 5px;
}
fieldset input{
flex-grow: 1;
}
fieldset input[type='submit']{
margin-right: 10px;
}
fieldset input[type='submit']:last-of-type{
margin-right: 0;
}
legend{
font-size: 14px;
background-color: #000;
padding: 0 3px;
color: #999;
}
/* menu
----------------------------------------------------------------------- */
#menu{
height: 100%;
width: 300px;
background-color: #333;
position: relative;
order: 1;
flex-grow: 0;
flex-shrink: 0;
margin: 0;
padding: 0;
top: 0;
overflow-y: overlay;
}
#menu p {
padding: 18px;
margin: 0;
border-bottom: 1px solid #444;
}
/* logo */
#h1{
color: #555;
text-decoration: none;
font-style: inherit;
}
.version {
color: #555;
font-size: inherit;
}
/* db select */
#dbs select{
width: 228px;
margin-left: 8px;
}
/* links */
#menu .links{
padding-top: 0;
padding-bottom: 10px;
}
#menu .links a:nth-child(even){
margin-left: 6px;
}
#menu .links a{
display: inline-block;
vertical-align: top;
width: 127px;
height: 31px;
margin: 0;
margin-bottom: 10px;
border: 1px solid #555;
line-height: 27px;
text-align: center;
text-transform: uppercase;
font-size: 12px;
border-radius: 3px;
color: #999;
}
#menu .links a.active,
#menu .links a:hover
{
border: 1px solid #ccc;
font-weight: normal;
color: inherit;
}
/* tables */
#logins, #tables{
border-bottom: none;
line-height: 20px;
padding: 18px 0;
overflow-y: auto !important;
}
#tables br{
display: none;
}
#tables a {
float: right;
padding: 5px 18px 9px;
line-height: 17px;
color: #2CC990;
font-size: 13px;
}
#tables .structure, #tables .view {
float: none;
display: block;
color: inherit;
font-size: 14px;
}
#logins a {
display: block;
padding: 5px 18px 9px;
color: inherit;
font-size: 14px;
}
#tables a.select.active,
#tables a.select:hover
{
color: #fba;
}
#logins a:hover,
#tables a[title]:hover,
#tables a.active,
#tables a.select:hover + a,
#tables a.select.active + a
{
background-color: #555;
font-weight: normal;
}
/* content
----------------------------------------------------------------------- */
#content{
height: 100%;
width: 100%;
margin: 0;
padding: 0;
padding-left: 50px;
padding-right: 50px;
padding-bottom: 30px;
overflow-y: auto !important;
order: 2;
flex-grow: 1;
}
#breadcrumb{
position: relative;
display: none;
}
#content h2{
margin-left: -50px;
}
/* links */
#content .links a,
code.jush-sql ~ a,
#fieldset-history > a:first-child
{
display: inline-block;
height: 32px;
line-height: 30px;
padding: 0 10px;
border: 1px solid #666;
border-radius: 3px;
font-size: 12px;
text-transform: uppercase;
}
#content .links a:hover,
code.jush-sql ~ a:hover,
#fieldset-history > a:first-child:hover
{
color: #eee;
border-color: #eee;
}
#ajaxstatus + *{
margin-top: 18px;
}
#ajaxstatus + *.links {
margin-top: 0 !important;
height: 65px;
line-height: 55px;
margin-bottom: 0;
}
#ajaxstatus + .links a{
white-space: nowrap;
margin-right: 20px;
padding: 0;
padding-bottom: 5px;
border: 0;
border-radius: 0;
font-size: 15px;
font-weight: bold;
}
#ajaxstatus + .links a.active,
#ajaxstatus + .links a:hover
{
border-bottom: 1px solid;
border-color: inherit;
color: inherit;
}
/* fieldset search */
#fieldset-search > div > *{
margin-right: 5px;
margin-bottom: 5px;
}
/* fieldset search */
#fieldset-partition p{
margin-bottom: 0;
}
/* feldset history */
#fieldset-history{
flex-wrap: wrap;
}
#fieldset-history i{
display: none;
}
#fieldset-history input[type='submit']{
flex-grow: 0;
order: 1;
margin-top: 1px;
margin-left: 17px;
}
#fieldset-history > div a:last-child{
order: 2;
}
#fieldset-history > a{
flex-grow: 0;
flex-basis: 5%;
min-width: 45px;
text-align: center;
margin-bottom: 10px;
margin-left: 5px;
}
#fieldset-history > .time{
flex-grow: 0;
flex-basis: 5%;
text-align: center;
line-height: 29px;
}
#fieldset-history > code{
flex-grow: 1;
flex-basis: 89%;
line-height: 29px;
}
#fieldset-history > .time{
flex-grow: 0;
flex-basis: 5%;
text-align: center;
}
/* sql
----------------------------------------------------------------------- */
.sqlarea{
border: 1px solid #444 !important;
width: 100% !important;
padding: 12px 15px !important;
font-size: 15px;
margin-bottom: 20px;
}
.jush-sql_code{
color: #fafafa !important;
font-family: 'Source Sans Pro', sans-serif !important;
}
.jush a, .jush a:visited{
color: #fba;
font-weight: normal;
}
.jush a:hover{
color: #fba;
cursor: pointer;
}
.jush-php_quo, .jush-quo, .jush-quo_one, .jush-php_eot, .jush-apo, .jush-sql_apo, .jush-sqlite_apo, .jush-sql_quo, .jush-sql_eot{
color: aquamarine;
}
.jush-bac, .jush-php_bac, .jush-bra, .jush-mssql_bra, .jush-sqlite_quo{
color: plum;
}
.jush-num, .jush-clr{
color: #85E2FF;
}
code {
background: #000;
font-size: 14px;
}
code.jush-sql ~ a{
position: relative;
margin-left: 5px;
/*margin-top: 20px;
margin-bottom: 20px; */
}
code.jush-sql ~ a:first-of-type{
margin-left: 30px;
}
code.jush-sql ~ a:first-of-type::before{
content: '◀';
color: #555;
position: absolute;
left: -22px;
font-size: 22px;
top: -1px;
}
/* logout form
----------------------------------------------------------------------- */
body > form{
position: absolute;
}

1795
site/public/adm/index.php Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.fi-pagination-items,.fi-pagination-overview,.fi-pagination-records-per-page-select:not(.fi-compact){display:none}@supports (container-type:inline-size){.fi-pagination{container-type:inline-size}@container (min-width: 28rem){.fi-pagination-records-per-page-select.fi-compact{display:none}.fi-pagination-records-per-page-select:not(.fi-compact){display:inline}}@container (min-width: 56rem){.fi-pagination:not(.fi-simple)>.fi-pagination-previous-btn{display:none}.fi-pagination-overview{display:inline}.fi-pagination:not(.fi-simple)>.fi-pagination-next-btn{display:none}.fi-pagination-items{display:flex}}}@supports not (container-type:inline-size){@media (min-width:640px){.fi-pagination-records-per-page-select.fi-compact{display:none}.fi-pagination-records-per-page-select:not(.fi-compact){display:inline}}@media (min-width:768px){.fi-pagination:not(.fi-simple)>.fi-pagination-previous-btn{display:none}.fi-pagination-overview{display:inline}.fi-pagination:not(.fi-simple)>.fi-pagination-next-btn{display:none}.fi-pagination-items{display:flex}}}.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{background-color:#333;border-radius:4px;color:#fff;font-size:14px;line-height:1.4;outline:0;position:relative;transition-property:transform,visibility,opacity;white-space:normal}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{border-top-color:initial;border-width:8px 8px 0;bottom:-7px;left:0;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{border-bottom-color:initial;border-width:0 8px 8px;left:0;top:-7px;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-left-color:initial;border-width:8px 0 8px 8px;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{border-right-color:initial;border-width:8px 8px 8px 0;left:-7px;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{color:#333;height:16px;width:16px}.tippy-arrow:before{border-color:transparent;border-style:solid;content:"";position:absolute}.tippy-content{padding:5px 9px;position:relative;z-index:1}.tippy-box[data-theme~=light]{background-color:#fff;box-shadow:0 0 20px 4px #9aa1b126,0 4px 80px -8px #24282f40,0 4px 4px -2px #5b5e6926;color:#26323d}.tippy-box[data-theme~=light][data-placement^=top]>.tippy-arrow:before{border-top-color:#fff}.tippy-box[data-theme~=light][data-placement^=bottom]>.tippy-arrow:before{border-bottom-color:#fff}.tippy-box[data-theme~=light][data-placement^=left]>.tippy-arrow:before{border-left-color:#fff}.tippy-box[data-theme~=light][data-placement^=right]>.tippy-arrow:before{border-right-color:#fff}.tippy-box[data-theme~=light]>.tippy-backdrop{background-color:#fff}.tippy-box[data-theme~=light]>.tippy-svg-arrow{fill:#fff}.fi-sortable-ghost{opacity:.3}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
function r({state:i}){return{state:i,rows:[],shouldUpdateRows:!0,init:function(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(t,e)=>{let s=o=>o===null?0:Array.isArray(o)?o.length:typeof o!="object"?0:Object.keys(o).length;s(t)===0&&s(e)===0||this.updateRows()})},addRow:function(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow:function(t){this.rows.splice(t,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows:function(t){let e=Alpine.raw(this.rows),s=e.splice(t.oldIndex,1)[0];e.splice(t.newIndex,0,s),this.rows=e,this.updateState()},updateRows:function(){if(!this.shouldUpdateRows){this.shouldUpdateRows=!0;return}let t=[];for(let[e,s]of Object.entries(this.state??{}))t.push({key:e,value:s});this.rows=t},updateState:function(){let t={};this.rows.forEach(e=>{e.key===""||e.key===null||(t[e.key]=e.value)}),this.shouldUpdateRows=!1,this.state=t}}}export{r as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
function i({state:a,splitKeys:n}){return{newTag:"",state:a,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(t){this.state=this.state.filter(e=>e!==t)},reorderTags:function(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{["x-on:blur"]:"createTag()",["x-model"]:"newTag",["x-on:keydown"](t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},["x-on:paste"](){this.$nextTick(()=>{if(n.length===0){this.createTag();return}let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default};

View File

@ -0,0 +1 @@
function t({initialHeight:e}){return{init:function(){this.render()},render:function(){this.$el.scrollHeight>0&&(this.$el.style.height=e+"rem",this.$el.style.height=this.$el.scrollHeight+"px")}}}export{t as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
function c(){return{collapsedGroups:[],isLoading:!1,selectedRecords:[],shouldCheckUniqueSelection:!0,init:function(){this.$wire.$on("deselectAllTableRecords",()=>this.deselectAllRecords()),this.$watch("selectedRecords",()=>{if(!this.shouldCheckUniqueSelection){this.shouldCheckUniqueSelection=!0;return}this.selectedRecords=[...new Set(this.selectedRecords)],this.shouldCheckUniqueSelection=!1})},mountBulkAction:function(e){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableBulkAction(e)},toggleSelectRecordsOnPage:function(){let e=this.getRecordsOnPage();if(this.areRecordsSelected(e)){this.deselectRecords(e);return}this.selectRecords(e)},toggleSelectRecordsInGroup:async function(e){if(this.isLoading=!0,this.areRecordsSelected(this.getRecordsInGroupOnPage(e))){this.deselectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e));return}this.selectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e)),this.isLoading=!1},getRecordsInGroupOnPage:function(e){let s=[];for(let t of this.$root.getElementsByClassName("fi-ta-record-checkbox"))t.dataset.group===e&&s.push(t.value);return s},getRecordsOnPage:function(){let e=[];for(let s of this.$root.getElementsByClassName("fi-ta-record-checkbox"))e.push(s.value);return e},selectRecords:function(e){for(let s of e)this.isRecordSelected(s)||this.selectedRecords.push(s)},deselectRecords:function(e){for(let s of e){let t=this.selectedRecords.indexOf(s);t!==-1&&this.selectedRecords.splice(t,1)}},selectAllRecords:async function(){this.isLoading=!0,this.selectedRecords=await this.$wire.getAllSelectableTableRecordKeys(),this.isLoading=!1},deselectAllRecords:function(){this.selectedRecords=[]},isRecordSelected:function(e){return this.selectedRecords.includes(e)},areRecordsSelected:function(e){return e.every(s=>this.isRecordSelected(s))},toggleCollapseGroup:function(e){if(this.isGroupCollapsed(e)){this.collapsedGroups.splice(this.collapsedGroups.indexOf(e),1);return}this.collapsedGroups.push(e)},isGroupCollapsed:function(e){return this.collapsedGroups.includes(e)},resetCollapsedGroups:function(){this.collapsedGroups=[]}}}export{c as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
User-agent: *
Disallow:
Disallow: /

7038
site/resources/css/style.css Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

1
site/resources/css/vendor/aos.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.nice-select{-webkit-tap-highlight-color:transparent;background-color:#fff;border-radius:5px;border:solid 1px #e8e8e8;-webkit-box-sizing:border-box;box-sizing:border-box;clear:both;cursor:pointer;display:block;float:left;font-family:inherit;font-size:14px;font-weight:normal;height:42px;line-height:40px;outline:0;padding-left:18px;padding-right:30px;position:relative;text-align:left !important;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;white-space:nowrap;width:auto}.nice-select:hover{border-color:#dbdbdb}.nice-select:active,.nice-select.open,.nice-select:focus{border-color:#999}.nice-select:after{border-bottom:2px solid #999;border-right:2px solid #999;content:'';display:block;height:5px;margin-top:-4px;pointer-events:none;position:absolute;right:12px;top:50%;-webkit-transform-origin:66% 66%;-ms-transform-origin:66% 66%;transform-origin:66% 66%;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg);-webkit-transition:all .15s ease-in-out;-o-transition:all .15s ease-in-out;transition:all .15s ease-in-out;width:5px}.nice-select.open:after{-webkit-transform:rotate(-135deg);-ms-transform:rotate(-135deg);transform:rotate(-135deg)}.nice-select.open .list{opacity:1;pointer-events:auto;-webkit-transform:scale(1) translateY(0);-ms-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}.nice-select.disabled{border-color:#ededed;color:#999;pointer-events:none}.nice-select.disabled:after{border-color:#ccc}.nice-select.wide{width:100%}.nice-select.wide .list{left:0 !important;right:0 !important}.nice-select.right{float:right}.nice-select.right .list{left:auto;right:0}.nice-select.small{font-size:12px;height:36px;line-height:34px}.nice-select.small:after{height:4px;width:4px}.nice-select.small .option{line-height:34px;min-height:34px}.nice-select .list{background-color:#fff;border-radius:5px;-webkit-box-shadow:0 0 0 1px rgba(68,68,68,0.11);box-shadow:0 0 0 1px rgba(68,68,68,0.11);-webkit-box-sizing:border-box;box-sizing:border-box;margin-top:4px;opacity:0;overflow:hidden;padding:0;pointer-events:none;position:absolute;top:100%;left:0;-webkit-transform-origin:50% 0;-ms-transform-origin:50% 0;transform-origin:50% 0;-webkit-transform:scale(0.75) translateY(-21px);-ms-transform:scale(0.75) translateY(-21px);transform:scale(0.75) translateY(-21px);-webkit-transition:all .2s cubic-bezier(0.5,0,0,1.25),opacity .15s ease-out;-o-transition:all .2s cubic-bezier(0.5,0,0,1.25),opacity .15s ease-out;transition:all .2s cubic-bezier(0.5,0,0,1.25),opacity .15s ease-out;z-index:9}.nice-select .list:hover .option:not(:hover){background-color:transparent !important}.nice-select .option{cursor:pointer;font-weight:400;line-height:40px;list-style:none;min-height:40px;outline:0;padding-left:18px;padding-right:29px;text-align:left;-webkit-transition:all .2s;-o-transition:all .2s;transition:all .2s}.nice-select .option:hover,.nice-select .option.focus,.nice-select .option.selected.focus{background-color:#f6f6f6}.nice-select .option.selected{font-weight:bold}.nice-select .option.disabled{background-color:transparent;color:#999;cursor:default}.no-csspointerevents .nice-select .list{display:none}.no-csspointerevents .nice-select.open .list{display:block}

View File

@ -0,0 +1 @@
@charset 'UTF-8';.slick-loading .slick-list{background:#fff url('../../img/ajax-loader.gif') center center no-repeat}@font-face{font-family:'slick';font-weight:normal;font-style:normal;src:url('../../fonts/slick.eot');src:url('../../fonts/slick.eot?#iefix') format('embedded-opentype'),url('../../fonts/slick.woff') format('woff'),url('../../fonts/slick.ttf') format('truetype'),url('../../fonts/slick.svg#slick') format('svg')}.slick-prev,.slick-next{font-size:0;line-height:0;position:absolute;top:50%;display:block;width:20px;height:20px;padding:0;-webkit-transform:translate(0,-50%);-ms-transform:translate(0,-50%);transform:translate(0,-50%);cursor:pointer;color:transparent;border:0;outline:0;background:transparent}.slick-prev:hover,.slick-prev:focus,.slick-next:hover,.slick-next:focus{color:transparent;outline:0;background:transparent}.slick-prev:hover:before,.slick-prev:focus:before,.slick-next:hover:before,.slick-next:focus:before{opacity:1}.slick-prev.slick-disabled:before,.slick-next.slick-disabled:before{opacity:.25}.slick-prev:before,.slick-next:before{font-family:'slick';font-size:20px;line-height:1;opacity:.75;color:white;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.slick-prev{left:-25px}[dir='rtl'] .slick-prev{right:-25px;left:auto}.slick-prev:before{content:'←'}[dir='rtl'] .slick-prev:before{content:'→'}.slick-next{right:-25px}[dir='rtl'] .slick-next{right:auto;left:-25px}.slick-next:before{content:'→'}[dir='rtl'] .slick-next:before{content:'←'}.slick-dotted.slick-slider{margin-bottom:30px}.slick-dots{position:absolute;bottom:-25px;display:block;width:100%;padding:0;margin:0;list-style:none;text-align:center}.slick-dots li{position:relative;display:inline-block;width:20px;height:20px;margin:0 5px;padding:0;cursor:pointer}.slick-dots li button{font-size:0;line-height:0;display:block;width:20px;height:20px;padding:5px;cursor:pointer;color:transparent;border:0;outline:0;background:transparent}.slick-dots li button:hover,.slick-dots li button:focus{outline:0}.slick-dots li button:hover:before,.slick-dots li button:focus:before{opacity:1}.slick-dots li button:before{font-family:'slick';font-size:6px;line-height:20px;position:absolute;top:0;left:0;width:20px;height:20px;content:'•';text-align:center;opacity:.25;color:black;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.slick-dots li.slick-active button:before{opacity:.75;color:black}

1
site/resources/css/vendor/slick.css vendored Normal file
View File

@ -0,0 +1 @@
.slick-slider{position:relative;display:block;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none;-khtml-user-select:none;-ms-touch-action:pan-y;touch-action:pan-y;-webkit-tap-highlight-color:transparent}.slick-list{position:relative;display:block;overflow:hidden;margin:0;padding:0}.slick-list:focus{outline:0}.slick-list.dragging{cursor:pointer;cursor:hand}.slick-slider .slick-track,.slick-slider .slick-list{-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.slick-track{position:relative;top:0;left:0;display:block;margin-left:auto;margin-right:auto}.slick-track:before,.slick-track:after{display:table;content:''}.slick-track:after{clear:both}.slick-loading .slick-track{visibility:hidden}.slick-slide{display:none;float:left;height:100%;min-height:1px}[dir='rtl'] .slick-slide{float:right}.slick-slide img{display:block}.slick-slide.slick-loading img{display:none}.slick-slide.dragging img{pointer-events:none}.slick-initialized .slick-slide{display:block}.slick-loading .slick-slide{visibility:hidden}.slick-vertical .slick-slide{display:block;height:auto;border:1px solid transparent}.slick-arrow.slick-hidden{display:none}

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More