feat: adding ability to optimize and import older site content.
This commit is contained in:
parent
5c218918ee
commit
9a7aa89362
1
.gitignore
vendored
1
.gitignore
vendored
@ -20,6 +20,7 @@ public_html/hot
|
||||
db/data/
|
||||
db/dump/
|
||||
static/dist/
|
||||
static/*
|
||||
|
||||
|
||||
storage/*.key
|
||||
|
3
Dockerfile
Normal file
3
Dockerfile
Normal file
@ -0,0 +1,3 @@
|
||||
FROM shinsenter/laravel:latest
|
||||
|
||||
RUN apt update && apt install -y jpegoptim optipng pngquant gifsicle webp libavif-bin
|
@ -3,7 +3,10 @@ version: '3'
|
||||
services:
|
||||
laravel-app:
|
||||
env_file: .env
|
||||
image: shinsenter/laravel:latest
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: ${CONTAINER_NAME:-pcag-laravel}
|
||||
volumes:
|
||||
- ./site:/var/www/html
|
||||
- ./static:/static
|
||||
@ -16,9 +19,9 @@ services:
|
||||
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
|
||||
LARAVEL_QUEUE_ENABLED: true
|
||||
LARAVEL_QUEUE_OPTIONS: --timeout=60 --tries=3 redis
|
||||
LARAVEL_SCHEDULE_ENABLED: true
|
||||
ports:
|
||||
- "80:80"
|
||||
links:
|
||||
@ -28,7 +31,7 @@ services:
|
||||
static:
|
||||
image: nginx:alpine
|
||||
volumes:
|
||||
- ./static:/usr/share/nginx/html:ro
|
||||
- ./static:/usr/share/nginx/html
|
||||
environment:
|
||||
TZ: UTC
|
||||
PUID: ${UID:-1000}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -3,13 +3,77 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\Artist;
|
||||
use App\Models\Episode;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
public function landing()
|
||||
{
|
||||
$user = auth()->user();
|
||||
return view('home.page');
|
||||
$headerCounters = $this->getHeaderCounters();
|
||||
$recentEpisodes = $this->mostRecentEpisodes();
|
||||
return view('home.page', [
|
||||
'user' => $user,
|
||||
'headerCounters' => $headerCounters,
|
||||
'recentEpisodes' => $recentEpisodes,
|
||||
]);
|
||||
}
|
||||
|
||||
private function mostRecentEpisodes()
|
||||
{
|
||||
$episodes = Cache::remember('latestEpisodes', 30, function() {
|
||||
return Episode::where('published', true)
|
||||
->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;
|
||||
}
|
||||
|
||||
}
|
||||
|
68
site/app/Jobs/ImportLegacyUserJob.php
Normal file
68
site/app/Jobs/ImportLegacyUserJob.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
125
site/app/Jobs/StashAndOptimizeLegacyArtworkJob.php
Normal file
125
site/app/Jobs/StashAndOptimizeLegacyArtworkJob.php
Normal 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('https://noagendaartgenerator.com' . $this->artwork->path . '/' . $this->artwork->filename)
|
||||
->resize(3000, null, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
})
|
||||
->encode('jpg', 100);
|
||||
$thumbImg = Image::make('https://noagendaartgenerator.com' . $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);
|
||||
}
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ public function podcast()
|
||||
|
||||
public function artwork()
|
||||
{
|
||||
return $this->hasOne(Artwork::class);
|
||||
return $this->hasOne(Artwork::class, 'id', 'artwork_id');
|
||||
}
|
||||
|
||||
public function artist()
|
||||
|
@ -11,7 +11,8 @@
|
||||
"laravel/framework": "^10.10",
|
||||
"laravel/sanctum": "^3.2",
|
||||
"laravel/tinker": "^2.8",
|
||||
"livewire/livewire": "^2.12"
|
||||
"livewire/livewire": "^2.12",
|
||||
"spatie/laravel-image-optimizer": "^1.7"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
|
579
site/composer.lock
generated
579
site/composer.lock
generated
File diff suppressed because it is too large
Load Diff
66
site/config/image-optimizer.php
Normal file
66
site/config/image-optimizer.php
Normal 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,
|
||||
];
|
@ -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'),
|
||||
|
@ -29,7 +29,7 @@ public function definition()
|
||||
'header' => fake()->imageUrl(270, 185),
|
||||
'location' => fake()->city() . ', ' . fake()->state(),
|
||||
'website' => rand(0, 1) ? fake()->url : null,
|
||||
'bio' => fake()->paragraphs(rand(1,3, true)),
|
||||
'bio' => fake()->paragraphs(rand(1, 3), true),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -27,14 +27,18 @@ 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,
|
||||
'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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,14 @@
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Artist;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\Episode;
|
||||
use App\Models\Podcast;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\Overlay;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class EpisodeFactory extends Factory
|
||||
{
|
||||
@ -24,11 +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(),
|
||||
'title' => fake()->name()
|
||||
'episode_date' => fake()->dateTimeThisDecade(),
|
||||
'slug' => $slug,
|
||||
'title' => $title,
|
||||
'mp3' => fake()->url(),
|
||||
'created_at' => $created,
|
||||
'updated_at' => $created,
|
||||
'legacy_id' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,23 @@
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\Podcast;
|
||||
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.
|
||||
*
|
||||
|
@ -4,10 +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.
|
||||
*
|
||||
|
@ -5,9 +5,22 @@
|
||||
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.
|
||||
*
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
60
site/database/seeders/EpisodeSeeder.php
Normal file
60
site/database/seeders/EpisodeSeeder.php
Normal 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;
|
||||
}
|
||||
}
|
31
site/database/seeders/FixLegacyEpisodeSeeder.php
Normal file
31
site/database/seeders/FixLegacyEpisodeSeeder.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
site/database/seeders/MapLegacyIdsSeeder.php
Normal file
17
site/database/seeders/MapLegacyIdsSeeder.php
Normal 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
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
@ -132,7 +132,14 @@ public function run()
|
||||
$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) {
|
||||
|
105
site/database/seeders/SeedFromOldApiSeeder.php
Normal file
105
site/database/seeders/SeedFromOldApiSeeder.php
Normal 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 = 283;
|
||||
$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();
|
||||
}
|
||||
}
|
||||
}
|
@ -64,6 +64,7 @@
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding-top: 50px;
|
||||
padding-bottom: 50px;
|
||||
@include lg-device {
|
||||
padding: 180px 0 170px;
|
||||
min-height: auto;
|
||||
@ -143,6 +144,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// hero banner style two
|
||||
.hero-banner-style-2 {
|
||||
.banner-content {
|
||||
|
@ -186,7 +186,7 @@ body {
|
||||
.bg-4,
|
||||
.bg-3,
|
||||
.bg-5 {
|
||||
background-image: none;
|
||||
background-image: url(../img/headerbg.jpg);
|
||||
background-color: $body-bg-light;
|
||||
}
|
||||
|
||||
|
21
site/resources/views/home/hero/banner-left.blade.php
Normal file
21
site/resources/views/home/hero/banner-left.blade.php
Normal file
@ -0,0 +1,21 @@
|
||||
<div class="banner-content">
|
||||
<h1 class="mb-5 title" data-aos="fade-up">Producing Album Art<br>
|
||||
<span>Live Since 2010</span>
|
||||
</h1>
|
||||
<p data-aos="fade-up" data-aos-delay="100">
|
||||
A community collaboration producing the best podcast album art in the universe!
|
||||
</p>
|
||||
<div class="group-btn mt-8" data-aos="fade-up" data-aos-delay="200">
|
||||
<a href="explore-filter-sidebar.html" class="btn btn-gradient">
|
||||
<span><i class="ri-rocket-line"></i>Explore</span>
|
||||
</a>
|
||||
<a href="create.html" class="btn btn-outline">
|
||||
<span><i class="ri-edit-line"></i> Create</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="counter-wrapper counter-wrapper-style-two">
|
||||
@foreach ($headerCounters as $headerCounterLabel => $headerCounterCount)
|
||||
@include('home.hero.counter')
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
7
site/resources/views/home/hero/counter.blade.php
Normal file
7
site/resources/views/home/hero/counter.blade.php
Normal file
@ -0,0 +1,7 @@
|
||||
<div class="counter-style-1" data-aos="fade-up" data-aos-delay="250">
|
||||
<div class="d-flex-center">
|
||||
<div class="number counter-item-active">{{ $headerCounterCount['number'] }}</div>
|
||||
<div class="count-kilo">{{ $headerCounterCount['unit'] }} <span>+</span></div>
|
||||
</div>
|
||||
<div class="counter-title">{{ $headerCounterLabel }}</div>
|
||||
</div>
|
43
site/resources/views/home/hero/slider/slide.blade.php
Normal file
43
site/resources/views/home/hero/slider/slide.blade.php
Normal file
@ -0,0 +1,43 @@
|
||||
<div class="explore-style-one">
|
||||
<div class="thumb">
|
||||
<a href="product-details.html"> <img src="{{ 'http://' . config('app.static_asset_url') . '/thumbnails/' . $recentEpisode->artwork->filename ?? '#'}}"
|
||||
alt="nft live auction thumbnail"></a>
|
||||
<!-- End .reaction-count -->
|
||||
</div>
|
||||
<!-- End .thumb -->
|
||||
<div class="content">
|
||||
<div class="header d-flex-between pt-4 pb-1">
|
||||
<h3 class="title">
|
||||
<a href="product-details.html">"{{ $recentEpisode->title }}"</a>
|
||||
</h3>
|
||||
<div class="more-dropdown "><i class="ri-more-fill" data-bs-toggle="dropdown"></i>
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<li><a class="dropdown-item" href="#">View Episode</a></li>
|
||||
<li><a class="dropdown-item" href="#">View Podcast</a></li>
|
||||
<li><a class="dropdown-item" href="#">View Show Notes</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="#">View Selected Artist</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- .header -->
|
||||
<div class="product-owner py-1 d-flex-between">
|
||||
<span class="bid-owner">Artwork Selected For<br>
|
||||
<strong><a href="#">{{ $recentEpisode->podcast->name }}</a></strong>
|
||||
<br>
|
||||
<a href="#">Episode {{ number_format($recentEpisode->episode_number + 0) }}</a></span>
|
||||
</div>
|
||||
<div class="product-owner py-1 d-flex-between">
|
||||
<span class="bid-owner">Artwork By<br><strong><a
|
||||
href="author-profile.html">{{ $recentEpisode->artwork->artist->name ?? 'Unknown' }}</a></strong></span>
|
||||
<span class="profile-share d-flex-center"><a href="author-profile.html" class="avatar" data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="{{ $recentEpisode->artwork->artist->name }}"><img src="{{ Vite::asset('resources/img/default_avatars/default_avatar_users_' . str_pad(rand(1, 32), 2, '0', STR_PAD_LEFT) . '.svg') }}"
|
||||
alt="{{ $recentEpisode->artwork->artist->name }}"></a></span>
|
||||
</div>
|
||||
<!-- End .product-owner -->
|
||||
</div>
|
||||
<!-- End .content -->
|
||||
</div>
|
7
site/resources/views/home/hero/slider/slider.blade.php
Normal file
7
site/resources/views/home/hero/slider/slider.blade.php
Normal file
@ -0,0 +1,7 @@
|
||||
<div class="slider slider-activation-banner-4 slick-gutter-15 slick-pagination-50">
|
||||
@foreach($recentEpisodes as $recentEpisode)
|
||||
@if ($recentEpisode->artwork)
|
||||
@include('home.hero.slider.slide')
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
@ -6,167 +6,10 @@
|
||||
<div class="container-fluid">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-xl-7 col-lg-6 col-md-12">
|
||||
<div class="banner-content">
|
||||
<h1 class="mb-5 title" data-aos="fade-up">Producing Album Art<br>
|
||||
<span>Live Since 2010</span>
|
||||
</h1>
|
||||
<p data-aos="fade-up" data-aos-delay="100">A community collaboration producing the best
|
||||
<br> album art in the universe!
|
||||
</p>
|
||||
<div class="group-btn mt-8" data-aos="fade-up" data-aos-delay="200">
|
||||
<a href="explore-filter-sidebar.html" class="btn btn-gradient"><span><i class="ri-rocket-line"></i>
|
||||
Explore</span></a>
|
||||
<a href="create.html" class="btn btn-outline"><span><i class="ri-edit-line"></i> Create</span></a>
|
||||
</div>
|
||||
<div class="counter-wrapper counter-wrapper-style-two">
|
||||
<div class="counter-style-1" data-aos="fade-up" data-aos-delay="250">
|
||||
<div class="d-flex-center">
|
||||
<div class="number counter-item-active">28</div>
|
||||
<div class="count-kilo">K <span>+</span></div>
|
||||
</div>
|
||||
<div class="counter-title">Artworks</div>
|
||||
</div>
|
||||
<!-- End .single-counter -->
|
||||
|
||||
<div class="counter-style-1" data-aos="fade-up" data-aos-delay="300">
|
||||
<div class="d-flex-center">
|
||||
<div class="number counter-item-active">2.9</div>
|
||||
<div class="count-kilo">K <span>+</span></div>
|
||||
</div>
|
||||
<div class="counter-title">Artists</div>
|
||||
</div>
|
||||
<!-- End .single-counter -->
|
||||
|
||||
<div class="counter-style-1" data-aos="fade-up" data-aos-delay="350">
|
||||
<div class="d-flex-center">
|
||||
<div class="number counter-item-active">1.6</div>
|
||||
<div class="count-kilo">K <span>+</span></div>
|
||||
</div>
|
||||
<div class="counter-title">Episodes</div>
|
||||
</div>
|
||||
<!-- End .single-counter -->
|
||||
</div>
|
||||
<!-- End .counter-wrapper -->
|
||||
</div>
|
||||
<!-- End banner-content -->
|
||||
|
||||
@include('home.hero.banner-left')
|
||||
</div>
|
||||
<!-- End .col -->
|
||||
<div class="col-xl-5 col-lg-6 col-md-12">
|
||||
<div class="slider slider-activation-banner-4 slick-gutter-15 slick-pagination-50">
|
||||
|
||||
<div class="explore-style-one">
|
||||
<div class="thumb">
|
||||
<a href="product-details.html"> <img src="https://noagendaartgenerator.com/assets/artwork/episode/1528/PDzxZp7SxR.png"
|
||||
alt="nft live auction thumbnail"></a>
|
||||
<button class="reaction-btn"><i class="ri-heart-fill"></i><span>49</span></button>
|
||||
<!-- End .reaction-count -->
|
||||
</div>
|
||||
<!-- End .thumb -->
|
||||
<div class="content">
|
||||
<div class="header d-flex-between pt-4 pb-3">
|
||||
<h3 class="title"><a href="product-details.html">No Agenda (1528)<br>"HABIDAT"</a></h3>
|
||||
<div class="more-dropdown "><i class="ri-more-fill" data-bs-toggle="dropdown"></i>
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<li><a class="dropdown-item" href="#">New bid</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="#">Refresh Metadata</a></li>
|
||||
<li><a class="dropdown-item" href="#">Share</a></li>
|
||||
<li><a class="dropdown-item" href="#">Report</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- .header -->
|
||||
<div class="product-owner py-4 d-flex-between">
|
||||
<span class="bid-owner">Artwork By<br><strong><a
|
||||
href="author-profile.html">KorrectDaRekard</a></strong></span>
|
||||
<span class="profile-share d-flex-center"><a href="author-profile.html" class="avatar" data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="Banuri Bari"><img src="{{ Vite::asset('resources/img/explore/avatar/1.png') }}" alt="Nft_Profile"></a></span>
|
||||
</div>
|
||||
<!-- End .product-owner -->
|
||||
</div>
|
||||
<!-- End .content -->
|
||||
</div>
|
||||
<!-- End explore-style-one -->
|
||||
|
||||
<div class="explore-style-one">
|
||||
<div class="thumb">
|
||||
<a href="product-details.html"> <img src="https://noagendaartgenerator.com/assets/artwork/episode/1527/obxPmNDIzeE.png"
|
||||
alt="nft live auction thumbnail"></a>
|
||||
<button class="reaction-btn"><i class="ri-heart-fill"></i><span>32</span></button>
|
||||
<!-- End .reaction-count -->
|
||||
</div>
|
||||
<!-- End .thumb -->
|
||||
<div class="content">
|
||||
<div class="header d-flex-between pt-4 pb-3">
|
||||
<h3 class="title"><a href="product-details.html">No Agenda (1527)<br>"Grip & Grin"</a></h3>
|
||||
<div class="more-dropdown "><i class="ri-more-fill" data-bs-toggle="dropdown"></i>
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<li><a class="dropdown-item" href="#">New bid</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="#">Refresh Metadata</a></li>
|
||||
<li><a class="dropdown-item" href="#">Share</a></li>
|
||||
<li><a class="dropdown-item" href="#">Report</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- .header -->
|
||||
<div class="product-owner py-4 d-flex-between">
|
||||
<span class="bid-owner">Artwork By<br><strong><a
|
||||
href="author-profile.html">Capitalist Agenda</a></strong></span>
|
||||
<span class="profile-share d-flex-center"><a href="author-profile.html" class="avatar" data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="Capitalist Agenda"><img src="{{ Vite::asset('resources/img/avatar/capitalistagenda.png') }}" style="width:100%;height:auto;" alt="Nft_Profile"></a></span>
|
||||
</div>
|
||||
<!-- End .product-owner -->
|
||||
</div>
|
||||
<!-- End .content -->
|
||||
</div>
|
||||
<!-- End explore-style-one -->
|
||||
|
||||
<div class="explore-style-one">
|
||||
<div class="thumb">
|
||||
<a href="product-details.html"> <img src="https://noagendaartgenerator.com/assets/artwork/episode/1528/PDzxZp7SxR.png"
|
||||
alt="nft live auction thumbnail"></a>
|
||||
<button class="reaction-btn"><i class="ri-heart-fill"></i><span>49</span></button>
|
||||
<!-- End .reaction-count -->
|
||||
</div>
|
||||
<!-- End .thumb -->
|
||||
<div class="content">
|
||||
<div class="header d-flex-between pt-4 pb-3">
|
||||
<h3 class="title"><a href="product-details.html">No Agenda<br>"HABIDAT"</a></h3>
|
||||
<div class="more-dropdown "><i class="ri-more-fill" data-bs-toggle="dropdown"></i>
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<li><a class="dropdown-item" href="#">New bid</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="#">Refresh Metadata</a></li>
|
||||
<li><a class="dropdown-item" href="#">Share</a></li>
|
||||
<li><a class="dropdown-item" href="#">Report</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- .header -->
|
||||
<div class="product-owner py-4 d-flex-between">
|
||||
<span class="bid-owner">Artwork By<br><strong><a
|
||||
href="author-profile.html">KorrectDaRekard</a></strong></span>
|
||||
<span class="profile-share d-flex-center"><a href="author-profile.html" class="avatar" data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="Banuri Bari"><img src="{{ Vite::asset('resources/img/explore/avatar/1.png') }}" alt="Nft_Profile"></a></span>
|
||||
</div>
|
||||
<!-- End .product-owner -->
|
||||
</div>
|
||||
<!-- End .content -->
|
||||
</div>
|
||||
<!-- End explore-style-one -->
|
||||
|
||||
</div>
|
||||
@include('home.hero.slider.slider')
|
||||
</div>
|
||||
<!-- End .col -->
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user