Update
This commit is contained in:
@@ -11,8 +11,8 @@ function user_is_master_admin(?User $user)
|
||||
return false;
|
||||
}
|
||||
|
||||
$emails = ['autopilotshorts@gmail.com', 'team@autopilotshorts.com', 'charles@exastellar.com'];
|
||||
$user_id = 1;
|
||||
$emails = ['autopilotshorts@gmail.com', 'team@autopilotshorts.com', 'charles@exastellar.com', 'charlesteh90@gmail.com'];
|
||||
$user_id = 0;
|
||||
|
||||
if ($user->id == $user_id) {
|
||||
return true;
|
||||
|
||||
191
app/Http/Controllers/AdminBackgroundGenerationController.php
Normal file
191
app/Http/Controllers/AdminBackgroundGenerationController.php
Normal file
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\BackgroundMedia;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class AdminBackgroundGenerationController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
// Load a random record with status 'pending_media'
|
||||
$pendingMedia = BackgroundMedia::where('status', 'pending_media')
|
||||
->whereIn('list_type', ['residential', 'commercial'])
|
||||
->inRandomOrder()
|
||||
->first();
|
||||
|
||||
return Inertia::render('admin/background-gen', [
|
||||
'pendingMedia' => $pendingMedia,
|
||||
]);
|
||||
}
|
||||
|
||||
public function generate(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'prompt' => 'required|string|max:1000',
|
||||
'media_id' => 'required|exists:background_medias,id',
|
||||
]);
|
||||
|
||||
$prompt = $request->input('prompt');
|
||||
|
||||
// Call Replicate API
|
||||
$replicateResponse = $this->callReplicateAPI($prompt);
|
||||
|
||||
if (! $replicateResponse || ! isset($replicateResponse['urls']['get'])) {
|
||||
return response()->json(['error' => 'Failed to start generation'], 500);
|
||||
}
|
||||
|
||||
// Poll for result
|
||||
$result = $this->pollReplicateResult($replicateResponse['urls']['get']);
|
||||
|
||||
if (! $result || ! isset($result['output'][0])) {
|
||||
return response()->json(['error' => 'Generation failed or timed out'], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'image_url' => $result['output'][0],
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
try {
|
||||
$media = BackgroundMedia::where('id', $id)
|
||||
->where('status', 'pending_media')
|
||||
->first();
|
||||
|
||||
if (! $media) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Pending media not found or already processed',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$media->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Media deleted successfully',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Error deleting pending media: '.$e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'An error occurred while deleting the media',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function save(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'media_id' => 'required|exists:background_medias,id',
|
||||
'image_url' => 'required|url',
|
||||
'prompt' => 'required|string',
|
||||
]);
|
||||
|
||||
$backgroundMedia = BackgroundMedia::findOrFail($request->input('media_id'));
|
||||
|
||||
// try {
|
||||
// Use MediaEngine to download and store the image
|
||||
$media = \App\Helpers\FirstParty\MediaEngine\MediaEngine::addMedia(
|
||||
'system-i',
|
||||
'image',
|
||||
'system_uploaded',
|
||||
'replicate',
|
||||
null,
|
||||
$request->input('image_url'),
|
||||
'download'
|
||||
);
|
||||
|
||||
// Update the background media record
|
||||
$backgroundMedia->update([
|
||||
'status' => 'completed',
|
||||
'media_uuid' => $media->uuid,
|
||||
'media_url' => \App\Helpers\FirstParty\MediaEngine\MediaEngine::getMediaCloudUrl($media),
|
||||
'prompt' => $request->input('prompt'),
|
||||
]);
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
// } catch (\Exception $e) {
|
||||
// return response()->json(['error' => 'Failed to save media: ' . $e->getMessage()], 500);
|
||||
// }
|
||||
}
|
||||
|
||||
private function callReplicateAPI($prompt)
|
||||
{
|
||||
$apiToken = config('services.replicate.api_token');
|
||||
|
||||
$data = [
|
||||
'input' => [
|
||||
'prompt' => $prompt,
|
||||
'go_fast' => true,
|
||||
'megapixels' => '1',
|
||||
'num_outputs' => 1,
|
||||
'aspect_ratio' => '1:1',
|
||||
'output_format' => 'webp',
|
||||
'output_quality' => 80,
|
||||
'num_inference_steps' => 4,
|
||||
],
|
||||
];
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, 'https://api.replicate.com/v1/models/black-forest-labs/flux-schnell/predictions');
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Authorization: Bearer '.$apiToken,
|
||||
'Content-Type: application/json',
|
||||
'Prefer: wait',
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 201) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
|
||||
private function pollReplicateResult($getUrl)
|
||||
{
|
||||
$maxAttempts = 30; // 5 minutes max (30 * 10 seconds)
|
||||
$attempt = 0;
|
||||
|
||||
while ($attempt < $maxAttempts) {
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $getUrl);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Authorization: Bearer '.config('services.replicate.api_token'),
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode === 200) {
|
||||
$result = json_decode($response, true);
|
||||
|
||||
if ($result['status'] === 'succeeded' && isset($result['output'][0])) {
|
||||
return $result;
|
||||
} elseif ($result['status'] === 'failed') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$attempt++;
|
||||
sleep(3); // Wait 10 seconds before next poll
|
||||
}
|
||||
|
||||
return false; // Timeout
|
||||
}
|
||||
}
|
||||
@@ -31,13 +31,11 @@ public function create(): Response
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
]);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\BackgroundMedia;
|
||||
use App\Models\MemeMedia;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -22,7 +23,7 @@ public function memes(Request $request)
|
||||
|
||||
public function background(Request $request)
|
||||
{
|
||||
$backgrounds = MemeMedia::where('type', 'image')->where('sub_type', 'background')->take('30')->inRandomOrder()->get();
|
||||
$backgrounds = BackgroundMedia::where('status', 'completed')->take('30')->inRandomOrder()->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => [
|
||||
|
||||
@@ -44,5 +44,6 @@ class BackgroundMedia extends Model
|
||||
'media_uuid',
|
||||
'media_url',
|
||||
'embedding',
|
||||
'prompt',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -49,4 +49,16 @@
|
||||
'description' => 'Audios uploaded by user.',
|
||||
'is_system' => false,
|
||||
],
|
||||
'system-i' => [
|
||||
'owner_type' => 'system',
|
||||
'location' => '/system-i/',
|
||||
'prefix' => 'si_',
|
||||
'postfix' => '',
|
||||
'extension' => 'png',
|
||||
'mime' => 'image/png',
|
||||
'media_type' => 'image',
|
||||
'name' => 'System Images',
|
||||
'description' => 'Images uploaded by system.',
|
||||
'is_system' => true,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -35,4 +35,8 @@
|
||||
],
|
||||
],
|
||||
|
||||
'replicate' => [
|
||||
'api_token' => env('REPLICATE_API_TOKEN'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -16,6 +16,7 @@ public function up(): void
|
||||
$table->string('list_type');
|
||||
$table->enum('area', ['interior', 'exterior']);
|
||||
$table->string('location_name');
|
||||
$table->text('prompt')->nullable();
|
||||
$table->enum('status', ['pending_media', 'completed'])->default('pending_media');
|
||||
$table->uuid('media_uuid')->nullable();
|
||||
$table->string('media_url')->nullable();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export default function AppLogo() {
|
||||
return (
|
||||
<>
|
||||
@@ -6,7 +5,7 @@ export default function AppLogo() {
|
||||
<AppLogoIcon className="size-5 fill-current text-white dark:text-black" />
|
||||
</div> */}
|
||||
<div className="ml-1 grid flex-1 text-left text-lg">
|
||||
<span className="mb-0.5 truncate leading-none font-semibold">Video² AI</span>
|
||||
<span className="mb-0.5 truncate leading-none font-semibold">Meme AI Gen</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,11 +2,10 @@ import { NavFooter } from '@/components/nav-footer';
|
||||
import { NavMain } from '@/components/nav-main';
|
||||
import { NavUser } from '@/components/nav-user';
|
||||
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
|
||||
import { type NavItem } from '@/types';
|
||||
import { SharedData, type NavItem } from '@/types';
|
||||
import { Link, usePage } from '@inertiajs/react';
|
||||
import { LayoutGrid, UserCog } from 'lucide-react';
|
||||
import { LayoutGrid, UserCog } from 'lucide-react';
|
||||
import AppLogo from './app-logo';
|
||||
import { SharedData } from '@/types';
|
||||
|
||||
const mainNavItems: NavItem[] = [
|
||||
{
|
||||
@@ -22,6 +21,11 @@ const adminMainNavItems: NavItem[] = [
|
||||
href: route('admin.dashboard'),
|
||||
icon: UserCog,
|
||||
},
|
||||
{
|
||||
title: 'Background Generation',
|
||||
href: route('admin.background-generation'),
|
||||
icon: UserCog,
|
||||
},
|
||||
];
|
||||
|
||||
const footerNavItems: NavItem[] = [
|
||||
@@ -38,7 +42,6 @@ const footerNavItems: NavItem[] = [
|
||||
];
|
||||
|
||||
export function AppSidebar() {
|
||||
|
||||
const { auth } = usePage<SharedData>().props;
|
||||
|
||||
return (
|
||||
@@ -60,8 +63,6 @@ export function AppSidebar() {
|
||||
{auth?.user_is_admin && <NavMain title="Admin" items={adminMainNavItems} />}
|
||||
</SidebarContent>
|
||||
|
||||
|
||||
|
||||
<SidebarFooter>
|
||||
<NavFooter items={footerNavItems} className="mt-auto" />
|
||||
<NavUser />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, VariantProps } from "class-variance-authority"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { PanelLeftIcon } from "lucide-react"
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
|
||||
271
resources/js/pages/admin/background-gen.tsx
Normal file
271
resources/js/pages/admin/background-gen.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import AppLayout from '@/layouts/app-layout';
|
||||
import { useAxios } from '@/plugins/AxiosContext';
|
||||
import { type BreadcrumbItem } from '@/types';
|
||||
import { Head } from '@inertiajs/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const breadcrumbs: BreadcrumbItem[] = [
|
||||
{
|
||||
title: 'Admin',
|
||||
href: route('admin.dashboard'),
|
||||
},
|
||||
{
|
||||
title: 'Background Generation',
|
||||
href: route('admin.background-generation'),
|
||||
},
|
||||
];
|
||||
|
||||
export default function BackgroundGeneration({ pendingMedia }) {
|
||||
const [prompt, setPrompt] = useState(pendingMedia?.prompt || '');
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [generatedImageUrl, setGeneratedImageUrl] = useState(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const axios = useAxios();
|
||||
|
||||
// Auto-assemble prompt when pendingMedia is available
|
||||
useEffect(() => {
|
||||
if (pendingMedia && pendingMedia.location_name && pendingMedia.list_type && pendingMedia.area) {
|
||||
const assembledPrompt =
|
||||
`${pendingMedia.location_name}, ${pendingMedia.list_type} ${pendingMedia.area}, wide angle shot, low angle view, sharp focus, cinematic composition`.toLowerCase();
|
||||
setPrompt(assembledPrompt);
|
||||
}
|
||||
}, [pendingMedia]);
|
||||
|
||||
// If no pending media, show completed state
|
||||
if (!pendingMedia) {
|
||||
return (
|
||||
<AppLayout breadcrumbs={breadcrumbs}>
|
||||
<Head title="Background Generation Complete" />
|
||||
<div className="flex h-full flex-1 flex-col items-center justify-center gap-6 rounded-xl p-6">
|
||||
<div className="max-w-md rounded-lg border bg-white p-8 text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-green-100">
|
||||
<svg className="h-8 w-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mb-2 text-2xl font-semibold text-gray-900">All Done!</h2>
|
||||
<p className="mb-6 text-gray-600">
|
||||
There are no pending media records to process. All background generation tasks have been completed.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="rounded-md bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!prompt.trim()) {
|
||||
alert('Please enter a prompt');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsGenerating(true);
|
||||
setGeneratedImageUrl(null);
|
||||
|
||||
try {
|
||||
const response = await axios.post(route('admin.background-generation.generate'), {
|
||||
prompt: prompt,
|
||||
media_id: pendingMedia.id,
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
setGeneratedImageUrl(response.data.image_url);
|
||||
} else {
|
||||
alert(response.data.error || 'Generation failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Generation error:', error);
|
||||
alert('Failed to generate image');
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!generatedImageUrl) {
|
||||
alert('No generated image to save');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
const response = await axios.post(route('admin.background-generation.save'), {
|
||||
media_id: pendingMedia.id,
|
||||
image_url: generatedImageUrl,
|
||||
prompt: prompt,
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
// Reload the page to start the cycle again
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(response.data.error || 'Failed to save');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Save error:', error);
|
||||
alert('Failed to save image');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!confirm('Are you sure you want to delete this pending media? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDeleting(true);
|
||||
|
||||
try {
|
||||
const response = await axios.post(route('admin.background-generation.delete', pendingMedia.id));
|
||||
|
||||
if (response.data.success) {
|
||||
// Reload the page to start the cycle again or show completed state
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(response.data.error || 'Failed to delete');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
alert('Failed to delete media');
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppLayout breadcrumbs={breadcrumbs}>
|
||||
<Head title="Background Generation" />
|
||||
<div className="flex h-full flex-1 flex-col gap-4 rounded-xl p-2 sm:p-4 lg:flex-row">
|
||||
{/* Left Column - Controls */}
|
||||
<div className="flex w-full flex-col gap-4 lg:w-1/2">
|
||||
{/* Pending Media Info */}
|
||||
<div className="rounded-lg border bg-white p-3 sm:p-4">
|
||||
<h2 className="mb-3 text-base font-semibold sm:text-lg">Pending Media Information</h2>
|
||||
<div className="grid grid-cols-1 gap-x-4 gap-y-2 text-sm sm:grid-cols-2">
|
||||
<div className="flex flex-wrap">
|
||||
<span className="font-medium text-gray-700">ID:</span>
|
||||
<span className="ml-2 break-all text-gray-900">{pendingMedia.id}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap">
|
||||
<span className="font-medium text-gray-700">List Type:</span>
|
||||
<span className="ml-2 text-gray-900">{pendingMedia.list_type}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap">
|
||||
<span className="font-medium text-gray-700">Area:</span>
|
||||
<span className="ml-2 text-gray-900">{pendingMedia.area}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap">
|
||||
<span className="font-medium text-gray-700">Location:</span>
|
||||
<span className="ml-2 break-words text-gray-900">{pendingMedia.location_name}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center">
|
||||
<span className="font-medium text-gray-700">Status:</span>
|
||||
<span className="ml-2 inline-flex items-center rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800">
|
||||
{pendingMedia.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap">
|
||||
<span className="font-medium text-gray-700">Created:</span>
|
||||
<span className="ml-2 text-gray-900">{new Date(pendingMedia.created_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 border-t border-gray-200 pt-3">
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting || isGenerating || isSaving}
|
||||
className="flex min-h-[44px] w-full items-center justify-center gap-1 rounded-md bg-red-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-red-700 disabled:bg-gray-400 sm:w-auto"
|
||||
>
|
||||
{isDeleting && <Spinner className="h-4 w-4 text-white" />}
|
||||
{isDeleting ? 'Deleting...' : 'Delete Media'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Generate Section */}
|
||||
<div className="rounded-lg border bg-white p-3 sm:p-4">
|
||||
<h2 className="mb-3 text-base font-semibold sm:text-lg">Generate Background Image</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="prompt" className="mb-2 block text-sm font-medium text-gray-700">
|
||||
Prompt
|
||||
</label>
|
||||
<textarea
|
||||
id="prompt"
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
className="min-h-[80px] w-full rounded-md border border-gray-300 px-3 py-3 text-sm shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
rows={4}
|
||||
placeholder="Enter your prompt for image generation..."
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={isGenerating || !prompt.trim()}
|
||||
className="flex min-h-[44px] w-full items-center justify-center gap-1 rounded-md bg-blue-600 px-4 py-3 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:bg-gray-400"
|
||||
>
|
||||
{isGenerating && <Spinner className="h-4 w-4 text-white" />}
|
||||
{isGenerating ? 'Generating...' : 'Generate'}
|
||||
</button>
|
||||
{generatedImageUrl && (
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="flex min-h-[44px] w-full items-center justify-center gap-1 rounded-md bg-green-600 px-4 py-3 text-sm font-medium text-white transition-colors hover:bg-green-700 disabled:bg-gray-400"
|
||||
>
|
||||
{isSaving && <Spinner className="h-4 w-4 text-white" />}
|
||||
{isSaving ? 'Saving...' : 'Save & Complete'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Image Preview */}
|
||||
<div className="order-first w-full lg:order-last lg:w-1/2">
|
||||
<div className="rounded-lg border bg-white p-3 sm:p-4">
|
||||
<h2 className="mb-3 text-base font-semibold sm:text-lg">Image Preview</h2>
|
||||
{/* Square aspect ratio container */}
|
||||
<div className="aspect-square w-full">
|
||||
<div className="flex h-full w-full items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50">
|
||||
{isGenerating ? (
|
||||
<div className="p-4 text-center">
|
||||
<Spinner className="h-8 w-8" />
|
||||
<p className="text-sm text-gray-600">Generating image...</p>
|
||||
</div>
|
||||
) : generatedImageUrl ? (
|
||||
<img src={generatedImageUrl} alt="Generated background" className="h-full w-full rounded-lg object-contain" />
|
||||
) : (
|
||||
<div className="p-4 text-center">
|
||||
<svg className="mx-auto h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="mt-2 text-sm text-gray-500">No image generated yet</p>
|
||||
<p className="text-xs text-gray-400">Click Generate to create an image</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import { Label } from '@/components/ui/label';
|
||||
import AuthLayout from '@/layouts/auth-layout';
|
||||
|
||||
type RegisterForm = {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
password_confirmation: string;
|
||||
@@ -18,7 +17,6 @@ type RegisterForm = {
|
||||
|
||||
export default function Register() {
|
||||
const { data, setData, post, processing, errors, reset } = useForm<Required<RegisterForm>>({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
@@ -36,30 +34,14 @@ export default function Register() {
|
||||
<Head title="Register" />
|
||||
<form className="flex flex-col gap-6" onSubmit={submit}>
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
required
|
||||
autoFocus
|
||||
tabIndex={1}
|
||||
autoComplete="name"
|
||||
value={data.name}
|
||||
onChange={(e) => setData('name', e.target.value)}
|
||||
disabled={processing}
|
||||
placeholder="Full name"
|
||||
/>
|
||||
<InputError message={errors.name} className="mt-2" />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email address</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
required
|
||||
tabIndex={2}
|
||||
autoFocus
|
||||
tabIndex={1}
|
||||
autoComplete="email"
|
||||
value={data.email}
|
||||
onChange={(e) => setData('email', e.target.value)}
|
||||
@@ -75,7 +57,7 @@ export default function Register() {
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
tabIndex={3}
|
||||
tabIndex={2}
|
||||
autoComplete="new-password"
|
||||
value={data.password}
|
||||
onChange={(e) => setData('password', e.target.value)}
|
||||
@@ -91,7 +73,7 @@ export default function Register() {
|
||||
id="password_confirmation"
|
||||
type="password"
|
||||
required
|
||||
tabIndex={4}
|
||||
tabIndex={3}
|
||||
autoComplete="new-password"
|
||||
value={data.password_confirmation}
|
||||
onChange={(e) => setData('password_confirmation', e.target.value)}
|
||||
@@ -101,7 +83,7 @@ export default function Register() {
|
||||
<InputError message={errors.password_confirmation} />
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="mt-2 w-full" tabIndex={5} disabled={processing}>
|
||||
<Button type="submit" className="mt-2 w-full" tabIndex={4} disabled={processing}>
|
||||
{processing && <LoaderCircle className="h-4 w-4 animate-spin" />}
|
||||
Create account
|
||||
</Button>
|
||||
@@ -109,7 +91,7 @@ export default function Register() {
|
||||
|
||||
<div className="text-muted-foreground text-center text-sm">
|
||||
Already have an account?{' '}
|
||||
<TextLink href={route('login')} tabIndex={6}>
|
||||
<TextLink href={route('login')} tabIndex={5}>
|
||||
Log in
|
||||
</TextLink>
|
||||
</div>
|
||||
|
||||
1
resources/js/types/index.d.ts
vendored
1
resources/js/types/index.d.ts
vendored
@@ -24,7 +24,6 @@ export interface NavItem {
|
||||
}
|
||||
|
||||
export interface SharedData {
|
||||
name: string;
|
||||
quote: { message: string; author: string };
|
||||
auth: Auth;
|
||||
ziggy: Config & { location: string };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const Ziggy = {"url":"https:\/\/memeaigen.com","port":null,"defaults":{},"routes":{"horizon.stats.index":{"uri":"horizon\/api\/stats","methods":["GET","HEAD"]},"horizon.workload.index":{"uri":"horizon\/api\/workload","methods":["GET","HEAD"]},"horizon.masters.index":{"uri":"horizon\/api\/masters","methods":["GET","HEAD"]},"horizon.monitoring.index":{"uri":"horizon\/api\/monitoring","methods":["GET","HEAD"]},"horizon.monitoring.store":{"uri":"horizon\/api\/monitoring","methods":["POST"]},"horizon.monitoring-tag.paginate":{"uri":"horizon\/api\/monitoring\/{tag}","methods":["GET","HEAD"],"parameters":["tag"]},"horizon.monitoring-tag.destroy":{"uri":"horizon\/api\/monitoring\/{tag}","methods":["DELETE"],"wheres":{"tag":".*"},"parameters":["tag"]},"horizon.jobs-metrics.index":{"uri":"horizon\/api\/metrics\/jobs","methods":["GET","HEAD"]},"horizon.jobs-metrics.show":{"uri":"horizon\/api\/metrics\/jobs\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.queues-metrics.index":{"uri":"horizon\/api\/metrics\/queues","methods":["GET","HEAD"]},"horizon.queues-metrics.show":{"uri":"horizon\/api\/metrics\/queues\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.jobs-batches.index":{"uri":"horizon\/api\/batches","methods":["GET","HEAD"]},"horizon.jobs-batches.show":{"uri":"horizon\/api\/batches\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.jobs-batches.retry":{"uri":"horizon\/api\/batches\/retry\/{id}","methods":["POST"],"parameters":["id"]},"horizon.pending-jobs.index":{"uri":"horizon\/api\/jobs\/pending","methods":["GET","HEAD"]},"horizon.completed-jobs.index":{"uri":"horizon\/api\/jobs\/completed","methods":["GET","HEAD"]},"horizon.silenced-jobs.index":{"uri":"horizon\/api\/jobs\/silenced","methods":["GET","HEAD"]},"horizon.failed-jobs.index":{"uri":"horizon\/api\/jobs\/failed","methods":["GET","HEAD"]},"horizon.failed-jobs.show":{"uri":"horizon\/api\/jobs\/failed\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.retry-jobs.show":{"uri":"horizon\/api\/jobs\/retry\/{id}","methods":["POST"],"parameters":["id"]},"horizon.jobs.show":{"uri":"horizon\/api\/jobs\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.index":{"uri":"horizon\/{view?}","methods":["GET","HEAD"],"wheres":{"view":"(.*)"},"parameters":["view"]},"sanctum.csrf-cookie":{"uri":"sanctum\/csrf-cookie","methods":["GET","HEAD"]},"api.app.memes":{"uri":"api\/app\/memes","methods":["POST"]},"api.app.background":{"uri":"api\/app\/background","methods":["POST"]},"dashboard":{"uri":"dashboard","methods":["GET","HEAD"]},"admin.dashboard":{"uri":"admin","methods":["GET","HEAD"]},"profile.edit":{"uri":"settings\/profile","methods":["GET","HEAD"]},"profile.update":{"uri":"settings\/profile","methods":["PATCH"]},"profile.destroy":{"uri":"settings\/profile","methods":["DELETE"]},"password.edit":{"uri":"settings\/password","methods":["GET","HEAD"]},"password.update":{"uri":"settings\/password","methods":["PUT"]},"appearance":{"uri":"settings\/appearance","methods":["GET","HEAD"]},"register":{"uri":"register","methods":["GET","HEAD"]},"login":{"uri":"login","methods":["GET","HEAD"]},"password.request":{"uri":"forgot-password","methods":["GET","HEAD"]},"password.email":{"uri":"forgot-password","methods":["POST"]},"password.reset":{"uri":"reset-password\/{token}","methods":["GET","HEAD"],"parameters":["token"]},"password.store":{"uri":"reset-password","methods":["POST"]},"verification.notice":{"uri":"verify-email","methods":["GET","HEAD"]},"verification.verify":{"uri":"verify-email\/{id}\/{hash}","methods":["GET","HEAD"],"parameters":["id","hash"]},"verification.send":{"uri":"email\/verification-notification","methods":["POST"]},"password.confirm":{"uri":"confirm-password","methods":["GET","HEAD"]},"logout":{"uri":"logout","methods":["POST"]},"home":{"uri":"\/","methods":["GET","HEAD"]},"test":{"uri":"tests","methods":["GET","HEAD"]},"storage.local":{"uri":"storage\/{path}","methods":["GET","HEAD"],"wheres":{"path":".*"},"parameters":["path"]}}};
|
||||
const Ziggy = {"url":"https:\/\/memeaigen.com","port":null,"defaults":{},"routes":{"horizon.stats.index":{"uri":"horizon\/api\/stats","methods":["GET","HEAD"]},"horizon.workload.index":{"uri":"horizon\/api\/workload","methods":["GET","HEAD"]},"horizon.masters.index":{"uri":"horizon\/api\/masters","methods":["GET","HEAD"]},"horizon.monitoring.index":{"uri":"horizon\/api\/monitoring","methods":["GET","HEAD"]},"horizon.monitoring.store":{"uri":"horizon\/api\/monitoring","methods":["POST"]},"horizon.monitoring-tag.paginate":{"uri":"horizon\/api\/monitoring\/{tag}","methods":["GET","HEAD"],"parameters":["tag"]},"horizon.monitoring-tag.destroy":{"uri":"horizon\/api\/monitoring\/{tag}","methods":["DELETE"],"wheres":{"tag":".*"},"parameters":["tag"]},"horizon.jobs-metrics.index":{"uri":"horizon\/api\/metrics\/jobs","methods":["GET","HEAD"]},"horizon.jobs-metrics.show":{"uri":"horizon\/api\/metrics\/jobs\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.queues-metrics.index":{"uri":"horizon\/api\/metrics\/queues","methods":["GET","HEAD"]},"horizon.queues-metrics.show":{"uri":"horizon\/api\/metrics\/queues\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.jobs-batches.index":{"uri":"horizon\/api\/batches","methods":["GET","HEAD"]},"horizon.jobs-batches.show":{"uri":"horizon\/api\/batches\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.jobs-batches.retry":{"uri":"horizon\/api\/batches\/retry\/{id}","methods":["POST"],"parameters":["id"]},"horizon.pending-jobs.index":{"uri":"horizon\/api\/jobs\/pending","methods":["GET","HEAD"]},"horizon.completed-jobs.index":{"uri":"horizon\/api\/jobs\/completed","methods":["GET","HEAD"]},"horizon.silenced-jobs.index":{"uri":"horizon\/api\/jobs\/silenced","methods":["GET","HEAD"]},"horizon.failed-jobs.index":{"uri":"horizon\/api\/jobs\/failed","methods":["GET","HEAD"]},"horizon.failed-jobs.show":{"uri":"horizon\/api\/jobs\/failed\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.retry-jobs.show":{"uri":"horizon\/api\/jobs\/retry\/{id}","methods":["POST"],"parameters":["id"]},"horizon.jobs.show":{"uri":"horizon\/api\/jobs\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.index":{"uri":"horizon\/{view?}","methods":["GET","HEAD"],"wheres":{"view":"(.*)"},"parameters":["view"]},"sanctum.csrf-cookie":{"uri":"sanctum\/csrf-cookie","methods":["GET","HEAD"]},"api.app.memes":{"uri":"api\/app\/memes","methods":["POST"]},"api.app.background":{"uri":"api\/app\/background","methods":["POST"]},"dashboard":{"uri":"dashboard","methods":["GET","HEAD"]},"admin.dashboard":{"uri":"admin","methods":["GET","HEAD"]},"admin.background-generation":{"uri":"admin\/background-generation","methods":["GET","HEAD"]},"admin.background-generation.generate":{"uri":"admin\/background-generation\/generate","methods":["POST"]},"admin.background-generation.save":{"uri":"admin\/background-generation\/save","methods":["POST"]},"admin.background-generation.delete":{"uri":"admin\/background-generation\/delete\/{id}","methods":["POST"],"parameters":["id"]},"profile.edit":{"uri":"settings\/profile","methods":["GET","HEAD"]},"profile.update":{"uri":"settings\/profile","methods":["PATCH"]},"profile.destroy":{"uri":"settings\/profile","methods":["DELETE"]},"password.edit":{"uri":"settings\/password","methods":["GET","HEAD"]},"password.update":{"uri":"settings\/password","methods":["PUT"]},"appearance":{"uri":"settings\/appearance","methods":["GET","HEAD"]},"register":{"uri":"register","methods":["GET","HEAD"]},"login":{"uri":"login","methods":["GET","HEAD"]},"password.request":{"uri":"forgot-password","methods":["GET","HEAD"]},"password.email":{"uri":"forgot-password","methods":["POST"]},"password.reset":{"uri":"reset-password\/{token}","methods":["GET","HEAD"],"parameters":["token"]},"password.store":{"uri":"reset-password","methods":["POST"]},"verification.notice":{"uri":"verify-email","methods":["GET","HEAD"]},"verification.verify":{"uri":"verify-email\/{id}\/{hash}","methods":["GET","HEAD"],"parameters":["id","hash"]},"verification.send":{"uri":"email\/verification-notification","methods":["POST"]},"password.confirm":{"uri":"confirm-password","methods":["GET","HEAD"]},"logout":{"uri":"logout","methods":["POST"]},"home":{"uri":"\/","methods":["GET","HEAD"]},"test":{"uri":"tests","methods":["GET","HEAD"]},"storage.local":{"uri":"storage\/{path}","methods":["GET","HEAD"],"wheres":{"path":".*"},"parameters":["path"]}}};
|
||||
if (typeof window !== 'undefined' && typeof window.Ziggy !== 'undefined') {
|
||||
Object.assign(Ziggy.routes, window.Ziggy.routes);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\AdminBackgroundGenerationController;
|
||||
use App\Http\Controllers\AdminDashboardController;
|
||||
use App\Http\Controllers\FrontHomeController;
|
||||
use App\Http\Controllers\UserDashboardController;
|
||||
@@ -14,6 +15,19 @@
|
||||
|
||||
Route::prefix('admin')->middleware([AdminMiddleware::class])->group(function () {
|
||||
Route::get('/', [AdminDashboardController::class, 'index'])->name('admin.dashboard');
|
||||
|
||||
Route::prefix('background-generation')->group(function () {
|
||||
Route::get('/', [AdminBackgroundGenerationController::class, 'index'])->name('admin.background-generation');
|
||||
|
||||
Route::post('generate', [AdminBackgroundGenerationController::class, 'generate'])
|
||||
->name('admin.background-generation.generate');
|
||||
|
||||
Route::post('save', [AdminBackgroundGenerationController::class, 'save'])
|
||||
->name('admin.background-generation.save');
|
||||
|
||||
Route::post('delete/{id}', [AdminBackgroundGenerationController::class, 'delete'])
|
||||
->name('admin.background-generation.delete');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@/*": ["./resources/js/*"]
|
||||
},
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"verbatimModuleSyntax": false,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
Reference in New Issue
Block a user