Compare commits
No commits in common. "eb0ec0e28775d7967c2960f7c61fce5a8820a637" and "f2fffc54c852dcb2310dfd1f375076df6ec3fc8c" have entirely different histories.
eb0ec0e287
...
f2fffc54c8
202 changed files with 564 additions and 9512 deletions
|
|
@ -1,32 +0,0 @@
|
|||
# Ignore .env files
|
||||
.env
|
||||
.env.*
|
||||
.envrc
|
||||
|
||||
# Ignore node_modules
|
||||
node_modules/
|
||||
|
||||
# Ignore vendor folder
|
||||
vendor/
|
||||
|
||||
# Ignore log files
|
||||
*.log
|
||||
|
||||
# Ignore IDE and editor files
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Ignore system files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Ignore Laravel storage
|
||||
/storage/*.key
|
||||
/storage/*.log
|
||||
/storage/framework/cache/*
|
||||
/storage/framework/sessions/*
|
||||
/storage/framework/views/*
|
||||
/storage/logs/*
|
||||
|
||||
# Ignore database files if in development
|
||||
database/database.sqlite
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
DUSK_HEADLESS_DISABLED=true
|
||||
ADMIN_EMAIL=login-test@example.com
|
||||
APP_ENV=testing
|
||||
DB_DATABASE=database/dusk.sqlite
|
||||
|
||||
APP_NAME=Laravel
|
||||
APP_KEY=base64:YOUR_GENERATED_KEY_HERE=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://127.0.0.1:8000
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
# PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
MEDIA_DISK=s3
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
|
|
@ -61,7 +61,5 @@ AWS_SECRET_ACCESS_KEY=
|
|||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
# AWS_BUCKET=share-lt-images
|
||||
# MEDIA_DISK=s3
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
|
|
|||
12
.github/copilot-instructions.md
vendored
12
.github/copilot-instructions.md
vendored
|
|
@ -13,11 +13,8 @@ This application is a Laravel application and its main Laravel ecosystems packag
|
|||
- laravel/fortify (FORTIFY) - v1
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/reverb (REVERB) - v1
|
||||
- laravel/sanctum (SANCTUM) - v4
|
||||
- livewire/flux (FLUXUI_FREE) - v2
|
||||
- livewire/livewire (LIVEWIRE) - v3
|
||||
- laravel/dusk (DUSK) - v8
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pint (PINT) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
|
|
@ -500,12 +497,3 @@ Fortify is a headless authentication backend that provides authentication routes
|
|||
- `Features::updatePasswords()` to let users change their passwords.
|
||||
- `Features::resetPasswords()` for password reset via email.
|
||||
</laravel-boost-guidelines>
|
||||
|
||||
|
||||
=== docker/core rules ===
|
||||
## Over-Engineering & Bloat
|
||||
- Do not add unnecessary boilerplate or "nice-to-have" features.
|
||||
- Only implement what solves the immediate problem.
|
||||
- Ask before adding optional infrastructure or configuration sections.
|
||||
- If a system worked before without something, don't add it "just in case".
|
||||
- Minimize configuration, complexity, and dependencies.
|
||||
|
|
|
|||
11
.gitignore
vendored
11
.gitignore
vendored
|
|
@ -9,7 +9,6 @@
|
|||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.env.dev
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
|
|
@ -22,13 +21,3 @@ yarn-error.log
|
|||
/.nova
|
||||
/.vscode
|
||||
/.zed
|
||||
.vite
|
||||
*.deleted
|
||||
.env.dusk.local
|
||||
log*.txt
|
||||
.envrc
|
||||
database/backups
|
||||
*backup.tar.gz
|
||||
public/css
|
||||
public/js
|
||||
notes
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
when:
|
||||
- event: push
|
||||
branch: dev
|
||||
steps:
|
||||
build-local:
|
||||
image: docker:24-dind
|
||||
privileged: true
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
commands:
|
||||
- echo "Pulling base images to ensure latest layers..."
|
||||
- docker pull --quiet php:8.4-fpm-alpine3.23 || true
|
||||
- echo "Try to pull previous image to use as cache ..."
|
||||
- docker pull quay.io/marshyon/share-lt:latest || true
|
||||
- echo "Building image for testing (amd64 only for CI compatibility)..."
|
||||
- docker build --platform linux/amd64 --cache-from=quay.io/marshyon/share-lt:latest -t share-lt:test .
|
||||
- echo "Tagging test image as quay.io/marshyon/share-lt:v0.0.8..."
|
||||
- docker tag share-lt:test quay.io/marshyon/share-lt:v0.0.8
|
||||
- echo "Generating SBOM..."
|
||||
- docker run --rm -v /var/run/docker.sock:/var/run/docker.sock anchore/syft:latest scan quay.io/marshyon/share-lt:v0.0.8 -o cyclonedx-json > sbom.json
|
||||
scan-vulnerabilities:
|
||||
image: aquasec/trivy:0.67.2
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
commands:
|
||||
- echo "Ensuring latest Trivy image is pulled..."
|
||||
- docker pull aquasec/trivy:latest || true
|
||||
- echo "Scanning for vulnerabilities via Docker daemon..."
|
||||
# Disabling scan for testing, will re-enable once a fix for
|
||||
# vulnerability is available.
|
||||
# Scan the image present in the Docker daemon; fail on CRITICAL severities
|
||||
# - trivy image --exit-code 1 --severity CRITICAL --no-progress share-lt:test
|
||||
# Run a full scan without failing just for logs
|
||||
- trivy image --severity HIGH,MEDIUM,LOW --no-progress share-lt:test
|
||||
- echo "Generating vulnerability report..."
|
||||
- trivy image --format cyclonedx --output trivy-vuln-bom.json share-lt:test
|
||||
- echo "Vulnerability Summary:"
|
||||
- trivy image --format table share-lt:test | tee trivy-vuln-summary.txt
|
||||
publish:
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
registry: quay.io
|
||||
repo: quay.io/marshyon/share-lt
|
||||
platforms: linux/amd64
|
||||
tags:
|
||||
- v0.0.8
|
||||
- latest
|
||||
username:
|
||||
from_secret: QUAY_USERNAME
|
||||
password:
|
||||
from_secret: QUAY_PASSWORD
|
||||
upload-sbom:
|
||||
image: cgr.dev/chainguard/cosign:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
COSIGN_REGISTRY_USERNAME:
|
||||
from_secret: QUAY_USERNAME
|
||||
COSIGN_REGISTRY_PASSWORD:
|
||||
from_secret: QUAY_PASSWORD
|
||||
commands:
|
||||
- cosign attach sbom --sbom sbom.json quay.io/marshyon/share-lt:v0.0.8 || echo "SBOM attach failed"
|
||||
- echo "Done - trivy report saved to workspace for manual review"
|
||||
|
||||
62
CHANGELOG.md
62
CHANGELOG.md
|
|
@ -1,67 +1,7 @@
|
|||
|
||||
# CHANGELOG
|
||||
|
||||
## 2026-02-17
|
||||
|
||||
added reverb, echo and toast messages for site builds
|
||||
|
||||
added change log for go-live logging and audit
|
||||
|
||||
## 2026-02-09
|
||||
|
||||
added reference compose files
|
||||
|
||||
added basic NATS integration
|
||||
|
||||
## 2026-01-25
|
||||
|
||||
added s3, docker build
|
||||
|
||||
## 2026-01-19
|
||||
|
||||
added text widgets
|
||||
|
||||
added categories
|
||||
|
||||
updated api to access text widgets and categories
|
||||
|
||||
added url and call to action for entries for use in cards
|
||||
|
||||
added import image and blogs import commands
|
||||
|
||||
## 2026-01-08
|
||||
|
||||
added tags to entry model
|
||||
|
||||
added text widget and category
|
||||
|
||||
## 2026-01-07
|
||||
|
||||
added simple API for entries model
|
||||
- to view entries
|
||||
- implement initial access control
|
||||
|
||||
this is sufficient to test static site generation
|
||||
|
||||
## 2026-01-06
|
||||
|
||||
added
|
||||
- Spatie Media Library
|
||||
- media library configuration file
|
||||
- Updated Entry model to support media handling
|
||||
- featured image upload with gallery selection and preview
|
||||
- login tests with Dusk for user authentication
|
||||
- Dusk test for featured image selection
|
||||
|
||||
## 2026-01-02
|
||||
|
||||
added initial model and filament resource
|
||||
|
||||
## 2026-01-01
|
||||
|
||||
added: laravel 12
|
||||
|
||||
added: AGPLv3
|
||||
|
||||
|
||||
|
||||
added: AGPLv3
|
||||
107
Dockerfile
107
Dockerfile
|
|
@ -1,107 +0,0 @@
|
|||
# Build stage for NATS CLI
|
||||
FROM golang:1.26-alpine AS nats-builder
|
||||
RUN apk add --no-cache git
|
||||
RUN git clone --depth 1 https://github.com/nats-io/natscli.git /src
|
||||
WORKDIR /src/nats
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o nats .
|
||||
|
||||
|
||||
|
||||
FROM php:8.4-fpm-alpine3.23
|
||||
|
||||
ENV APP_ENV=production
|
||||
ENV APP_DEBUG=false
|
||||
WORKDIR /var/www
|
||||
RUN apk update && apk add --no-cache \
|
||||
build-base \
|
||||
libpng-dev \
|
||||
libjpeg-turbo-dev \
|
||||
freetype-dev \
|
||||
zip \
|
||||
jpegoptim optipng pngquant gifsicle \
|
||||
vim \
|
||||
unzip \
|
||||
git \
|
||||
curl \
|
||||
libzip-dev \
|
||||
oniguruma-dev \
|
||||
nodejs \
|
||||
npm \
|
||||
icu-dev \
|
||||
sqlite-dev \
|
||||
sqlite-libs \
|
||||
nginx \
|
||||
supervisor \
|
||||
su-exec \
|
||||
tini \
|
||||
unzip \
|
||||
bash \
|
||||
jq \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
COPY --from=nats-builder /src/nats/nats /usr/local/bin/nats
|
||||
RUN chmod +x /usr/local/bin/nats
|
||||
|
||||
RUN rm -rf /var/cache/apk/*
|
||||
RUN docker-php-ext-install mbstring zip exif pcntl intl gd pdo pdo_sqlite bcmath
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY cmd/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# Copy supervisord configuration
|
||||
COPY ./docker/supervisord.conf /etc/supervisord.conf
|
||||
RUN mkdir -p /var/log/supervisor \
|
||||
&& mkdir -p /run/nginx /var/cache/nginx /var/lib/nginx /var/tmp/nginx \
|
||||
&& chown -R root:root /run/nginx /var/cache/nginx /var/lib/nginx /var/tmp/nginx
|
||||
|
||||
# Create www user and add to www-data group
|
||||
RUN adduser -u 1000 -G www-data -s /bin/sh -D www
|
||||
|
||||
# Configure PHP-FPM to run as www user
|
||||
RUN sed -i 's/user = www-data/user = www/g' /usr/local/etc/php-fpm.d/www.conf
|
||||
|
||||
# Remove the semicolon to uncomment the listen directive
|
||||
RUN sed -i 's/;listen = 127.0.0.1:9000/listen = 9000/' /usr/local/etc/php-fpm.d/www.conf
|
||||
|
||||
# Ensure the worker running the code is correct (usually www-data or nginx)
|
||||
RUN sed -i 's/;listen.owner = www-data/listen.owner = www/' /usr/local/etc/php-fpm.d/www.conf
|
||||
RUN sed -i 's/;listen.group = www-data/listen.group = www-data/' /usr/local/etc/php-fpm.d/www.conf
|
||||
|
||||
# Update nginx.conf to use 'www' user instead of 'nginx'
|
||||
RUN sed -i 's/user nginx;/user www;/' /etc/nginx/nginx.conf
|
||||
|
||||
# Remove user and group directives from nginx and php-fpm configs to avoid conflicts
|
||||
RUN sed -i '/^user /d' /etc/nginx/nginx.conf
|
||||
RUN sed -i '/^user = /d' /usr/local/etc/php-fpm.d/www.conf
|
||||
RUN sed -i '/^group = /d' /usr/local/etc/php-fpm.d/www.conf
|
||||
|
||||
# Set permissions for nginx directories
|
||||
RUN mkdir -p /var/lib/nginx/tmp/client_body /var/log/nginx \
|
||||
&& chown -R www:www-data /var/lib/nginx /var/log/nginx \
|
||||
&& chmod -R 755 /var/lib/nginx /var/log/nginx \
|
||||
&& touch /run/nginx/nginx.pid \
|
||||
&& chown www:www-data /run/nginx/nginx.pid
|
||||
|
||||
# Copy application code (includes database/migrations/) and excluding
|
||||
# files in .dockerignore
|
||||
COPY --chown=www:www-data . /var/www
|
||||
RUN chown -R www:www-data /var/www
|
||||
RUN chown -R www:www-data /var/log/supervisor
|
||||
|
||||
# Switch to www user
|
||||
USER www
|
||||
|
||||
# Install app dependencies
|
||||
RUN composer install --optimize-autoloader --no-dev
|
||||
RUN npm ci
|
||||
RUN npm run build
|
||||
|
||||
# run laravel cache optimization
|
||||
RUN php artisan optimize
|
||||
|
||||
EXPOSE 8889
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/docker-entrypoint.sh"]
|
||||
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf"]
|
||||
|
|
@ -13,11 +13,8 @@ This application is a Laravel application and its main Laravel ecosystems packag
|
|||
- laravel/fortify (FORTIFY) - v1
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/reverb (REVERB) - v1
|
||||
- laravel/sanctum (SANCTUM) - v4
|
||||
- livewire/flux (FLUXUI_FREE) - v2
|
||||
- livewire/livewire (LIVEWIRE) - v3
|
||||
- laravel/dusk (DUSK) - v8
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pint (PINT) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -219,7 +219,7 @@ If you develop a new program, and you want it to be of the greatest possible use
|
|||
|
||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
share-lt
|
||||
test
|
||||
Copyright (C) 2026 jon
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
|
|
|||
44
README.md
44
README.md
|
|
@ -1,48 +1,6 @@
|
|||
# share-lt
|
||||
|
||||
Share Light CMS - Headless CMS with Real-time Publishing
|
||||
|
||||
[](https://wpk.headshed.dev/repos/2) [](https://quay.io/repository/marshyon/share-lt)
|
||||
|
||||
## Overview
|
||||
|
||||
Share Light is a modern headless CMS built with Laravel 12 that enables real-time content management and automated static site generation. It provides a complete backend solution for content creators and developers who need a robust, scalable CMS with live preview capabilities.
|
||||
|
||||
## Architecture
|
||||
|
||||
This application runs as a **fat container** orchestrated by **tini** and **supervisor**, managing multiple services:
|
||||
|
||||
- **Laravel 12** - Core CMS application with Filament admin interface
|
||||
- **PHP-FPM** - PHP process manager
|
||||
- **nginx** - Web server and reverse proxy
|
||||
- **Queue Worker** - Background job processing
|
||||
- **Laravel Reverb** - WebSocket server for real-time communication
|
||||
|
||||
### Integration with External Services
|
||||
|
||||
- **NATS Messaging** - Publishes messages to NATS streams when content changes
|
||||
- **share-lt-astro-consumer** - Companion application that consumes NATS messages to rebuild static sites
|
||||
- **Real-time Notifications** - Uses Reverb to send toast alerts to users about build status
|
||||
|
||||
## Features
|
||||
|
||||
### Content Management
|
||||
- **Entry Model** - Core content entities with media library support
|
||||
- **Text Widgets** - Reusable content blocks
|
||||
- **Categories** - Content organization and taxonomy
|
||||
- **Media Library** - Image and file management with Spatie Media Library
|
||||
|
||||
### Real-time Publishing Workflow
|
||||
1. **Content Updates** - When entries are modified, messages are automatically sent to NATS streams
|
||||
2. **Consumer Notifications** - External applications (like share-lt-astro-consumer) listen for these messages
|
||||
3. **Preview Generation** - Consumers rebuild preview websites based on content changes
|
||||
4. **Status Updates** - Build results are sent back via API
|
||||
5. **Live Alerts** - Users receive real-time toast notifications about build status through Reverb
|
||||
|
||||
### API & Integration
|
||||
- **RESTful API** - Complete API for content retrieval and management
|
||||
- **Access Control** - Built-in authorization and authentication
|
||||
- **NATS Integration** - Message streaming for distributed architecture
|
||||
Share Light CMS
|
||||
|
||||
this project is in 'Alpha'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,144 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Models\Entry;
|
||||
|
||||
class ImportBlogs extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:import-blogs';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
|
||||
/*
|
||||
|
||||
{
|
||||
"id": 7,
|
||||
"title": "Low/No Code FlowiseAI",
|
||||
"slug": "lowno-code-flowiseai",
|
||||
"content": "<p><img alt=\"\" src=\"http://127.0.0.1:8000/storage/99/ZsTHWAQRbdvgRUEwGmCIsm4TyChIjBwiY71VmnnR.webp\"/></p>\n<p>In <a href=\"https://flowiseai.com/\">FlowiseAI</a>, applications based on the JavaScript fork of LangChain are modeled in a 'no/low-code' environment. If you are coming from a closed source world, yet trying to implement devops principles this may fill you with fear, dread and uncertainty. FlowiseAI offers the advantage of using plain-text JSON files to represent each workflow. These files are easy to understand, open, and readily backup-able, unlike opaque proprietary binary formats\nThe data used at runtime and other component prerequisites like credentials are stored in the FlowiseAI data volume, which looks like this</p>\n<p><code>bash\nMode LastWriteTime ..... 8ZS1C0ZBB.webp",
|
||||
"created_at": "2024-12-17 11:24:59",
|
||||
"updated_at": "2024-12-17 12:53:54",
|
||||
"category_id": 1,
|
||||
"blog_date": "2024-02-26 00:00",
|
||||
"is_featured": 0,
|
||||
"published": 1
|
||||
},
|
||||
|
||||
*/
|
||||
|
||||
$filePath = '/home/user/projects/laravel/12/media_library/boring-astro-static/imported_database/blogs.json';
|
||||
|
||||
if (!file_exists($filePath)) {
|
||||
$this->error("File not found: $filePath");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$jsonContent = file_get_contents($filePath);
|
||||
$blogs = json_decode($jsonContent, true);
|
||||
|
||||
if (!$blogs) {
|
||||
$this->error("Could not parse JSON file: $filePath");
|
||||
return 1;
|
||||
}
|
||||
|
||||
foreach ($blogs as $blog) {
|
||||
// Only process the blog with ID 51
|
||||
if (($blog['id'] ?? null) !== 51) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$slug = $blog['slug'] ?? null;
|
||||
$this->info("Processing blog ID: {$blog['id']} with slug: {$slug}");
|
||||
// Check if the entry already exists
|
||||
$existingEntry = Entry::where('slug', $slug)->first();
|
||||
|
||||
if ($existingEntry) {
|
||||
// Update existing entry with cleaned content
|
||||
$existingEntry->update([
|
||||
'content' => $this->cleanHtmlForFilament($blog['content'] ?? ''),
|
||||
'description' => $this->extractPlainTextFromHtml($blog['content'] ?? ''),
|
||||
]);
|
||||
$this->info("Updated content for: {$existingEntry->title}");
|
||||
} else {
|
||||
// Create new entry
|
||||
Entry::create([
|
||||
'title' => $blog['title'] ?? null,
|
||||
'slug' => $slug,
|
||||
'description' => $this->extractPlainTextFromHtml($blog['content'] ?? ''),
|
||||
'is_published' => $blog['published'] ?? false,
|
||||
'is_featured' => $blog['is_featured'] ?? false,
|
||||
'published_at' => $blog['blog_date'] ?? null,
|
||||
'content' => $this->cleanHtmlForFilament($blog['content'] ?? ''),
|
||||
'category_id' => 1, // Default category
|
||||
]);
|
||||
$this->info("Created new entry: " . ($blog['title'] ?? 'Untitled'));
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Blogs imported successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract plain text from HTML for description field
|
||||
*/
|
||||
private function extractPlainTextFromHtml(string $html): string
|
||||
{
|
||||
if (empty($html)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Decode HTML entities and strip tags
|
||||
$text = html_entity_decode(strip_tags($html));
|
||||
|
||||
// Clean up whitespace
|
||||
$text = preg_replace('/\s+/', ' ', $text);
|
||||
|
||||
// Limit to reasonable length for description
|
||||
return trim(substr($text, 0, 500));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean HTML content for Filament rich editor
|
||||
*/
|
||||
private function cleanHtmlForFilament(string $html): string
|
||||
{
|
||||
if (empty($html)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Convert escaped newlines to actual newlines
|
||||
$html = str_replace(['\\n', '\\r\\n', '\\r'], "\n", $html);
|
||||
|
||||
// Decode HTML entities
|
||||
$html = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
// Clean up excessive whitespace but preserve paragraph structure
|
||||
$html = preg_replace('/\s*\n\s*/', ' ', $html);
|
||||
$html = preg_replace('/[ \t]+/', ' ', $html);
|
||||
|
||||
// Ensure paragraphs have proper spacing
|
||||
$html = str_replace('</p><p>', '</p>' . "\n" . '<p>', $html);
|
||||
|
||||
return trim($html);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Entry;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ImportImages extends Command
|
||||
{
|
||||
protected $signature = 'app:import-images {--entry-id=1}';
|
||||
protected $description = 'Import images into the media library';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$entryId = $this->option('entry-id');
|
||||
$entry = Entry::find($entryId);
|
||||
|
||||
if (!$entry) {
|
||||
$this->error("Entry with ID {$entryId} not found");
|
||||
return;
|
||||
}
|
||||
|
||||
$files = Storage::disk('public')->files('imported_images');
|
||||
|
||||
if (empty($files)) {
|
||||
$this->info('No files found in storage/app/public/imported_images/');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info("Found " . count($files) . " files to import");
|
||||
|
||||
foreach ($files as $filePath) {
|
||||
try {
|
||||
$fullPath = storage_path('app/public/' . $filePath);
|
||||
$fileName = pathinfo($fullPath, PATHINFO_BASENAME);
|
||||
|
||||
if (!file_exists($fullPath)) {
|
||||
$this->error("File not found: {$fullPath}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if already exists
|
||||
$existingMedia = $entry->getMedia()->where('file_name', $fileName)->first();
|
||||
if ($existingMedia) {
|
||||
$this->info("Skipping existing: {$fileName}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$media = $entry->addMedia($fullPath)
|
||||
->toMediaCollection('default');
|
||||
|
||||
$this->info("Imported: {$fileName}");
|
||||
} catch (\Exception $e) {
|
||||
$this->error("Failed to import {$filePath}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Import completed');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\Entry;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
use function Livewire\str;
|
||||
|
||||
class RemediateBlogS3Images extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:remediate-blog-s3-images';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$stringToFind = 'src="http://127.0.0.1:8000/storage';
|
||||
$stringToReplace = 'src="https://your-s3-bucket-here/yours-site-static-media-dir-here';
|
||||
|
||||
Log::info('RemediateBlogS3Images command executed.');
|
||||
foreach (Entry::all() as $entry) {
|
||||
|
||||
$this->info('Entry ID: ' . $entry->id);
|
||||
if (str_contains($entry->content, $stringToFind)) {
|
||||
$this->info(' - Found occurrence in entry ID: ' . $entry->id);
|
||||
// Extract all image srcs that match the pattern
|
||||
preg_match_all('/src=\"http:\/\/127.0.0.1:8000\/storage([^\"]*)/', $entry->content, $matches);
|
||||
if (!empty($matches[0])) {
|
||||
foreach ($matches[0] as $i => $foundUrl) {
|
||||
$this->info(' - Found image src: ' . $foundUrl);
|
||||
// Compute the replacement for this specific image
|
||||
$relativePath = $matches[1][$i] ?? '';
|
||||
$newUrl = 'src="https://your-s3-bucket-here/yours-site-static-media-dir-here' . $relativePath;
|
||||
$this->info(' - Will replace with: ' . $newUrl);
|
||||
}
|
||||
}
|
||||
$updatedContent = \str_replace($stringToFind, $stringToReplace, $entry->content);
|
||||
// uncomment the following when your sure about the changes
|
||||
// $entry->content = $updatedContent;
|
||||
// $entry->save();
|
||||
// $this->info(' - Updated entry ID: ' . $entry->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Events\PreviewSiteBuilt;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SendPreviewSiteBuiltNotification extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'test:preview-site-built {--message=Preview site is built}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Send a test notification that the preview site is built';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$message = $this->option('message');
|
||||
|
||||
$this->info("Command :: Broadcasting preview site built notification: {$message}");
|
||||
|
||||
PreviewSiteBuilt::dispatch($message, 'success');
|
||||
|
||||
$this->info('Notification broadcasted successfully!');
|
||||
$this->info('Check your Filament admin panel for the toast notification.');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class PreviewSiteBuilt implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public string $message = 'Preview site is built',
|
||||
public string $type = 'success'
|
||||
) {}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new Channel('filament-notifications'),
|
||||
];
|
||||
}
|
||||
|
||||
public function broadcastAs(): string
|
||||
{
|
||||
return 'preview-site.built';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class TestEvent implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public string $message = 'Test message from Laravel'
|
||||
) {}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new Channel('test-channel'),
|
||||
];
|
||||
}
|
||||
|
||||
public function broadcastAs(): string
|
||||
{
|
||||
return 'test.message';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Assets;
|
||||
|
||||
use App\Filament\Resources\Assets\Pages\CreateAsset;
|
||||
use App\Filament\Resources\Assets\Pages\EditAsset;
|
||||
use App\Filament\Resources\Assets\Pages\ListAssets;
|
||||
use App\Filament\Resources\Assets\Schemas\AssetForm;
|
||||
use App\Filament\Resources\Assets\Tables\AssetsTable;
|
||||
use App\Models\Asset;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class AssetResource extends Resource
|
||||
{
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
protected static ?string $model = Asset::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::ArrowUpTray;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return AssetForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return AssetsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListAssets::route('/'),
|
||||
'create' => CreateAsset::route('/create'),
|
||||
'edit' => EditAsset::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Assets\Pages;
|
||||
|
||||
use App\Filament\Resources\Assets\AssetResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateAsset extends CreateRecord
|
||||
{
|
||||
protected static string $resource = AssetResource::class;
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Assets\Pages;
|
||||
|
||||
use App\Filament\Resources\Assets\AssetResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditAsset extends EditRecord
|
||||
{
|
||||
protected static string $resource = AssetResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Assets\Pages;
|
||||
|
||||
use App\Filament\Resources\Assets\AssetResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListAssets extends ListRecords
|
||||
{
|
||||
protected static string $resource = AssetResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Assets\Schemas;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class AssetForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('alt_text'),
|
||||
SpatieMediaLibraryFileUpload::make('image')
|
||||
->collection('default')
|
||||
->image()
|
||||
->disk('s3')
|
||||
->visibility('public')
|
||||
->label('Upload Image'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Assets\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class AssetsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('alt_text')
|
||||
->searchable(),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Categroys;
|
||||
|
||||
use App\Filament\Resources\Categroys\Pages\CreateCategroy;
|
||||
use App\Filament\Resources\Categroys\Pages\EditCategroy;
|
||||
use App\Filament\Resources\Categroys\Pages\ListCategroys;
|
||||
use App\Filament\Resources\Categroys\Schemas\CategroyForm;
|
||||
use App\Filament\Resources\Categroys\Tables\CategroysTable;
|
||||
use App\Models\Category;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CategroyResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Category::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::RectangleGroup;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return CategroyForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return CategroysTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListCategroys::route('/'),
|
||||
'create' => CreateCategroy::route('/create'),
|
||||
'edit' => EditCategroy::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Categroys\Pages;
|
||||
|
||||
use App\Filament\Resources\Categroys\CategroyResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateCategroy extends CreateRecord
|
||||
{
|
||||
protected static string $resource = CategroyResource::class;
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Categroys\Pages;
|
||||
|
||||
use App\Filament\Resources\Categroys\CategroyResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditCategroy extends EditRecord
|
||||
{
|
||||
protected static string $resource = CategroyResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Categroys\Pages;
|
||||
|
||||
use App\Filament\Resources\Categroys\CategroyResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListCategroys extends ListRecords
|
||||
{
|
||||
protected static string $resource = CategroyResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Categroys\Schemas;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class CategroyForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->label('Category Name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Categroys\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CategroysTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->label('Category Name')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Changes;
|
||||
|
||||
use App\Filament\Resources\Changes\Pages\CreateChange;
|
||||
use App\Filament\Resources\Changes\Pages\EditChange;
|
||||
use App\Filament\Resources\Changes\Pages\ListChanges;
|
||||
use App\Filament\Resources\Changes\Schemas\ChangeForm;
|
||||
use App\Filament\Resources\Changes\Tables\ChangesTable;
|
||||
use App\Models\Change as ModelsChange;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ChangeResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ModelsChange::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::ArrowUpOnSquareStack;
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return 'Settings';
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return ChangeForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return ChangesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListChanges::route('/'),
|
||||
'create' => CreateChange::route('/create'),
|
||||
'edit' => EditChange::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Changes\Pages;
|
||||
|
||||
use App\Filament\Resources\Changes\ChangeResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateChange extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ChangeResource::class;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$data['user_id'] = auth()->id();
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Changes\Pages;
|
||||
|
||||
use App\Filament\Resources\Changes\ChangeResource;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditChange extends EditRecord
|
||||
{
|
||||
protected static string $resource = ChangeResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Changes\Pages;
|
||||
|
||||
use App\Filament\Resources\Changes\ChangeResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListChanges extends ListRecords
|
||||
{
|
||||
protected static string $resource = ChangeResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Changes\Schemas;
|
||||
|
||||
use Filament\Forms\Components\Select as FormSelect;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class ChangeForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Textarea::make('note')
|
||||
->label('Note')
|
||||
->required(),
|
||||
FormSelect::make('type')
|
||||
->label('Type')
|
||||
->options([
|
||||
'go-live' => 'Go Live',
|
||||
'general' => 'General',
|
||||
])
|
||||
->default('go-live')
|
||||
->required(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Changes\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ChangesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('note')
|
||||
->label('Note')
|
||||
->wrap(),
|
||||
TextColumn::make('type')
|
||||
->label('Type'),
|
||||
TextColumn::make('user.name')
|
||||
->label('User'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Entries;
|
||||
|
||||
use App\Filament\Resources\Entries\Pages\CreateEntry;
|
||||
use App\Filament\Resources\Entries\Pages\EditEntry;
|
||||
use App\Filament\Resources\Entries\Pages\ListEntries;
|
||||
use App\Filament\Resources\Entries\Pages\ViewEntry;
|
||||
use App\Filament\Resources\Entries\Schemas\EntryForm;
|
||||
use App\Filament\Resources\Entries\Schemas\EntryInfolist;
|
||||
use App\Filament\Resources\Entries\Tables\EntriesTable;
|
||||
use App\Models\Entry;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class EntryResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Entry::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::PencilSquare;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'title';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return EntryForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function infolist(Schema $schema): Schema
|
||||
{
|
||||
return EntryInfolist::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return EntriesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListEntries::route('/'),
|
||||
'create' => CreateEntry::route('/create'),
|
||||
'view' => ViewEntry::route('/{record}'),
|
||||
'edit' => EditEntry::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Entries\Pages;
|
||||
|
||||
use App\Filament\Resources\Entries\EntryResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateEntry extends CreateRecord
|
||||
{
|
||||
protected static string $resource = EntryResource::class;
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Entries\Pages;
|
||||
|
||||
use App\Filament\Resources\Entries\EntryResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditEntry extends EditRecord
|
||||
{
|
||||
protected static string $resource = EntryResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
ViewAction::make(),
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Entries\Pages;
|
||||
|
||||
use App\Filament\Resources\Entries\EntryResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListEntries extends ListRecords
|
||||
{
|
||||
protected static string $resource = EntryResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Entries\Pages;
|
||||
|
||||
use App\Filament\Resources\Entries\EntryResource;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewEntry extends ViewRecord
|
||||
{
|
||||
protected static string $resource = EntryResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,544 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Entries\Schemas;
|
||||
|
||||
use App\Models\Category;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
|
||||
use Filament\Forms\Components\SpatieTagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
|
||||
class EntryForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Select::make('type')
|
||||
->options([
|
||||
'article' => 'Article',
|
||||
'card' => 'Card',
|
||||
'text' => 'Text',
|
||||
'image' => 'Image',
|
||||
])
|
||||
->default('article')
|
||||
->required()
|
||||
->live(),
|
||||
TextInput::make('title')
|
||||
->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function ($state, $set): void {
|
||||
$set('slug', Str::slug((string) $state));
|
||||
}),
|
||||
TextInput::make('slug')
|
||||
->required()
|
||||
->visible(fn($get) => $get('type') === 'article')
|
||||
->dehydrated()
|
||||
->readOnly(),
|
||||
Textarea::make('description')
|
||||
->visible(fn($get) => $get('type') === 'article')
|
||||
->columnSpanFull(),
|
||||
SpatieTagsInput::make('tags')
|
||||
->type('entry-tags')
|
||||
->visible(fn($get) => $get('type') === 'article')
|
||||
->columnSpanFull(),
|
||||
SpatieMediaLibraryFileUpload::make('featured_image')
|
||||
->multiple() // <- force array handling for Filament v4 bug
|
||||
->visible(
|
||||
fn($get) =>
|
||||
$get('type') === 'article' || $get('type') === 'image'
|
||||
)
|
||||
->collection('featured-image')
|
||||
->image()
|
||||
->imageEditor()
|
||||
->disk(config('media-library.disk_name', 'public'))
|
||||
->visibility('public')
|
||||
->columnSpanFull()
|
||||
->dehydrated(false)
|
||||
->saveUploadedFileUsing(function ($file, $record) {
|
||||
if (is_array($file)) {
|
||||
$file = reset($file);
|
||||
}
|
||||
|
||||
// Validate upload object early
|
||||
if (
|
||||
! is_object($file) ||
|
||||
! (method_exists($file, 'getRealPath') || method_exists($file, 'getPathname') || method_exists($file, 'getStream') || method_exists($file, 'store'))
|
||||
) {
|
||||
Log::error('Invalid upload object', ['type' => gettype($file)]);
|
||||
throw new \Exception('Invalid upload object provided to saveUploadedFileUsing');
|
||||
}
|
||||
|
||||
// Use safe variables for further calls
|
||||
$realPath = method_exists($file, 'getRealPath') ? $file->getRealPath() : null;
|
||||
$exists = $realPath ? file_exists($realPath) : false;
|
||||
$name = method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : null;
|
||||
|
||||
|
||||
|
||||
|
||||
Log::info('TemporaryUploadedFile Debug', [
|
||||
'path' => $file->getRealPath(),
|
||||
'exists' => file_exists($file->getRealPath()),
|
||||
'name' => $file->getClientOriginalName(),
|
||||
'temp_dir' => sys_get_temp_dir(),
|
||||
'disk_root' => config('filesystems.disks.local.root'),
|
||||
'is_readable' => is_readable($file->getRealPath()),
|
||||
'is_writable' => is_writable($file->getRealPath()),
|
||||
]);
|
||||
|
||||
// Additional debug: Check if the file is being moved to livewire-tmp
|
||||
$livewireTmpPath = storage_path('framework/livewire-tmp');
|
||||
Log::info('Livewire Temp Directory Debug', [
|
||||
'livewire_tmp_path' => $livewireTmpPath,
|
||||
'exists' => file_exists($livewireTmpPath),
|
||||
'is_writable' => is_writable($livewireTmpPath),
|
||||
]);
|
||||
|
||||
// Check if the file is being moved
|
||||
$tempFilePath = $file->getRealPath();
|
||||
$newFilePath = $livewireTmpPath . '/' . $file->getClientOriginalName();
|
||||
if (file_exists($tempFilePath)) {
|
||||
Log::info('File exists in temp directory', ['temp_file_path' => $tempFilePath]);
|
||||
} else {
|
||||
Log::error('File does not exist in temp directory', ['temp_file_path' => $tempFilePath]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// $diskName = config('media-library.disk_name', 'public');
|
||||
$diskName = config('media-library.disk_name');
|
||||
|
||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||
Log::info('Featured Image Upload Debug', [
|
||||
'disk' => $diskName,
|
||||
'file_name' => $file->getClientOriginalName(),
|
||||
'file_size' => $file->getSize(),
|
||||
'file_mime' => $file->getMimeType(),
|
||||
'file_path' => $file->getRealPath(),
|
||||
'record_id' => $record?->id,
|
||||
'aws_config' => [
|
||||
'bucket' => config('filesystems.disks.s3.bucket'),
|
||||
'region' => config('filesystems.disks.s3.region'),
|
||||
'key_exists' => !empty(config('filesystems.disks.s3.key')),
|
||||
'secret_exists' => !empty(config('filesystems.disks.s3.secret')),
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$record) {
|
||||
throw new \Exception('Record not found during upload');
|
||||
}
|
||||
|
||||
// Test S3 connection if using S3
|
||||
if ($diskName === 's3') {
|
||||
$disk = \Storage::disk('s3');
|
||||
|
||||
// Test basic S3 connectivity
|
||||
$testFile = 'test-' . time() . '.txt';
|
||||
$disk->put($testFile, 'test content');
|
||||
$disk->delete($testFile);
|
||||
|
||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||
Log::info('S3 connectivity test passed');
|
||||
}
|
||||
}
|
||||
|
||||
// Use addMedia with the file directly, not addMediaFromRequest
|
||||
// Generate secure filename similar to Livewire temp files
|
||||
$originalName = $file->getClientOriginalName();
|
||||
$extension = pathinfo($originalName, PATHINFO_EXTENSION);
|
||||
$baseName = pathinfo($originalName, PATHINFO_FILENAME);
|
||||
|
||||
// Generate secure filename with encoded original name
|
||||
$encodedName = base64_encode($originalName);
|
||||
$secureFileName = Str::random(32) . '-meta' . $encodedName . '-.' . $extension;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Resolve possibly-relative Livewire temp path + safe fallbacks
|
||||
$realPath = $realPath ?: (method_exists($file, 'getRealPath') ? $file->getRealPath() : null);
|
||||
$candidates = [];
|
||||
|
||||
if ($realPath && str_starts_with($realPath, '/')) {
|
||||
$candidates[] = $realPath;
|
||||
} else {
|
||||
$candidates[] = sys_get_temp_dir() . '/' . ltrim((string)$realPath, '/');
|
||||
$candidates[] = storage_path('framework/' . ltrim((string)$realPath, '/'));
|
||||
$candidates[] = storage_path(ltrim((string)$realPath, '/'));
|
||||
$candidates[] = base_path(ltrim((string)$realPath, '/'));
|
||||
}
|
||||
|
||||
if ($realPath) {
|
||||
$candidates[] = storage_path('framework/livewire-tmp/' . basename($realPath));
|
||||
$candidates[] = sys_get_temp_dir() . '/' . basename($realPath);
|
||||
}
|
||||
|
||||
// 1) Try storing to local disk (creates an absolute path we control)
|
||||
$stored = null;
|
||||
if (method_exists($file, 'store')) {
|
||||
try {
|
||||
$stored = $file->store('livewire-temp', 'local'); // storage/app/livewire-temp/...
|
||||
if ($stored && \Storage::disk('local')->exists($stored)) {
|
||||
$resolved = \Storage::disk('local')->path($stored);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::debug('store() fallback failed', ['err' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) If not resolved, check candidates
|
||||
if (! isset($resolved)) {
|
||||
foreach ($candidates as $p) {
|
||||
if ($p && file_exists($p)) {
|
||||
$resolved = $p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) If still not resolved, try stream -> temp file copy
|
||||
$is_tmp_copy = false;
|
||||
if (! isset($resolved)) {
|
||||
try {
|
||||
$stream = null;
|
||||
if (method_exists($file, 'getStream')) {
|
||||
$stream = $file->getStream();
|
||||
} elseif (method_exists($file, 'getRealPath') && is_readable($file->getRealPath())) {
|
||||
$stream = fopen($file->getRealPath(), 'r');
|
||||
}
|
||||
|
||||
if ($stream) {
|
||||
$tmpPath = tempnam(sys_get_temp_dir(), 'filament-upload-');
|
||||
$out = fopen($tmpPath, 'w');
|
||||
stream_copy_to_stream($stream, $out);
|
||||
fclose($out);
|
||||
if (is_resource($stream)) {
|
||||
@fclose($stream);
|
||||
}
|
||||
$resolved = $tmpPath;
|
||||
$is_tmp_copy = true;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::debug('stream fallback failed', ['err' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Still nothing -> error
|
||||
if (empty($resolved)) {
|
||||
Log::error('Featured Image Upload: could not resolve temp path', [
|
||||
'original' => $realPath,
|
||||
'checked_candidates' => $candidates,
|
||||
'stored' => $stored,
|
||||
]);
|
||||
throw new \Exception("File `{$realPath}` does not exist");
|
||||
}
|
||||
|
||||
// 5) Use resolved absolute path
|
||||
$media = $record->addMedia($resolved)
|
||||
->usingName($baseName)
|
||||
->usingFileName($secureFileName)
|
||||
->toMediaCollection('featured-image', $diskName);
|
||||
|
||||
// 6) Cleanup short-lived artifacts
|
||||
if (! empty($is_tmp_copy) && file_exists($resolved)) {
|
||||
@unlink($resolved);
|
||||
}
|
||||
if (! empty($stored) && \Storage::disk('local')->exists($stored)) {
|
||||
\Storage::disk('local')->delete($stored);
|
||||
}
|
||||
|
||||
Log::info('Featured image resolved', ['resolved' => $resolved, 'media_id' => $media->id ?? null]);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||
Log::info('Featured Image Upload Success', [
|
||||
'media_id' => $media->id,
|
||||
'media_url' => $media->getUrl(),
|
||||
'media_path' => $media->getPathRelativeToRoot(),
|
||||
'disk' => $media->disk
|
||||
]);
|
||||
}
|
||||
|
||||
return $media->getUrl();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Featured Image Upload Failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'disk' => $diskName,
|
||||
'file_name' => $file->getClientOriginalName()
|
||||
]);
|
||||
|
||||
|
||||
// Also show error to user
|
||||
\Filament\Notifications\Notification::make()
|
||||
->danger()
|
||||
->title('Upload Failed')
|
||||
->body($e->getMessage())
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
})
|
||||
->hintAction(
|
||||
Action::make('featured_picker')
|
||||
->label('Featured Image from Gallery')
|
||||
->icon('heroicon-m-photo')
|
||||
->extraAttributes(['id' => 'featured-picker-button'])
|
||||
->schema([
|
||||
Select::make('image_id')
|
||||
->label('Select an existing image')
|
||||
->allowHtml()
|
||||
->options(function () {
|
||||
$currentDisk = config('media-library.disk_name', 'public');
|
||||
return Media::where('disk', $currentDisk)
|
||||
->latest()
|
||||
->limit(50)
|
||||
->get(['id', 'file_name', 'name', 'disk'])
|
||||
->mapWithKeys(function (Media $item) {
|
||||
try {
|
||||
$url = $item->getUrl();
|
||||
$fileName = e($item->file_name);
|
||||
$name = e($item->name ?? '');
|
||||
|
||||
$html = "<div class='flex items-center gap-3'>" .
|
||||
"<img src='{$url}' class='rounded' style='width:60px;height:60px;object-fit:cover;' alt='{$fileName}' loading='lazy' />" .
|
||||
"<div class='flex flex-col'>" .
|
||||
"<span class='font-medium text-sm'>{$name}</span>" .
|
||||
"<span class='text-xs text-gray-500'>{$fileName}</span>" .
|
||||
'</div></div>';
|
||||
|
||||
return [$item->id => $html];
|
||||
} catch (\Exception $e) {
|
||||
return [];
|
||||
}
|
||||
})->toArray();
|
||||
})
|
||||
->searchable()
|
||||
->preload()
|
||||
->required(),
|
||||
])
|
||||
->action(function (array $data, SpatieMediaLibraryFileUpload $component): void {
|
||||
$record = $component->getRecord();
|
||||
$diskName = config('media-library.disk_name', 'public');
|
||||
|
||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||
Log::info('Featured Image Picker Action Debug', [
|
||||
'disk' => $diskName,
|
||||
'record_id' => $record?->id,
|
||||
'image_id' => $data['image_id'] ?? null
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $record) {
|
||||
\Filament\Notifications\Notification::make()
|
||||
->warning()
|
||||
->title('Save the entry first')
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $data['image_id']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sourceMedia = Media::find($data['image_id']);
|
||||
if (! $sourceMedia) {
|
||||
Log::error('Source media not found', ['image_id' => $data['image_id']]);
|
||||
\Filament\Notifications\Notification::make()
|
||||
->danger()
|
||||
->title('Source image not found in database')
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// For S3, we need to handle file copying differently
|
||||
if ($sourceMedia->disk === 's3') {
|
||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||
Log::info('Copying S3 media to new collection', [
|
||||
'source_disk' => $sourceMedia->disk,
|
||||
'source_path' => $sourceMedia->getPathRelativeToRoot(),
|
||||
'target_disk' => $diskName
|
||||
]);
|
||||
}
|
||||
|
||||
// Copy from S3 to S3 or download temporarily
|
||||
$sourceDisk = \Storage::disk($sourceMedia->disk);
|
||||
$sourceContent = $sourceDisk->get($sourceMedia->getPathRelativeToRoot());
|
||||
|
||||
if (!$sourceContent) {
|
||||
throw new \Exception('Could not read source file from S3');
|
||||
}
|
||||
|
||||
$tempCopy = sys_get_temp_dir() . '/' . uniqid() . '_' . $sourceMedia->file_name;
|
||||
file_put_contents($tempCopy, $sourceContent);
|
||||
} else {
|
||||
// Local file handling
|
||||
$sourceFile = $sourceMedia->getPath();
|
||||
if (! file_exists($sourceFile)) {
|
||||
throw new \Exception('Source file not found on disk');
|
||||
}
|
||||
|
||||
$tempCopy = sys_get_temp_dir() . '/' . uniqid() . '_' . $sourceMedia->file_name;
|
||||
copy($sourceFile, $tempCopy);
|
||||
}
|
||||
|
||||
// Verify record has ID
|
||||
if (! $record->id) {
|
||||
\Filament\Notifications\Notification::make()
|
||||
->danger()
|
||||
->title('Entry must be saved first')
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the copy to the entry's featured-image collection
|
||||
$newMedia = $record->addMedia($tempCopy)
|
||||
->usingName($sourceMedia->name ?: pathinfo($sourceMedia->file_name, PATHINFO_FILENAME))
|
||||
->usingFileName($sourceMedia->file_name)
|
||||
->toMediaCollection('featured-image', $diskName);
|
||||
|
||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||
Log::info('Featured Image Picker Success', [
|
||||
'new_media_id' => $newMedia->id,
|
||||
'new_media_disk' => $newMedia->disk,
|
||||
'new_media_url' => $newMedia->getUrl()
|
||||
]);
|
||||
}
|
||||
|
||||
// Dispatch event for app.js to handle
|
||||
$component->getLivewire()->dispatch('featured-image-added', ['mediaId' => $newMedia->id]);
|
||||
|
||||
\Filament\Notifications\Notification::make()
|
||||
->success()
|
||||
->title('Image added to featured image')
|
||||
->send();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Featured Image Picker Failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'source_media_id' => $data['image_id'],
|
||||
'disk' => $diskName
|
||||
]);
|
||||
|
||||
\Filament\Notifications\Notification::make()
|
||||
->danger()
|
||||
->title('Error: ' . $e->getMessage())
|
||||
->persistent()
|
||||
->send();
|
||||
} finally {
|
||||
if (isset($tempCopy) && file_exists($tempCopy)) {
|
||||
unlink($tempCopy);
|
||||
}
|
||||
}
|
||||
})
|
||||
),
|
||||
Toggle::make('is_published')
|
||||
->required(),
|
||||
Toggle::make('is_featured')
|
||||
->required(),
|
||||
TextInput::make('priority')
|
||||
->label('Priority')
|
||||
->numeric()
|
||||
->default(0)
|
||||
->required(),
|
||||
DatePicker::make('published_at')
|
||||
->visible(fn($get) => $get('type') === 'article'),
|
||||
Select::make('category_id')
|
||||
->label('Category')
|
||||
->options(function () {
|
||||
return Category::all()
|
||||
->pluck('name', 'id')
|
||||
->toArray();
|
||||
})
|
||||
->searchable(),
|
||||
TextInput::make('call_to_action_text')
|
||||
->label('Call to Action Text')
|
||||
->visible(fn($get) => $get('type') !== 'article'),
|
||||
TextInput::make('call_to_action_link')
|
||||
->label('Call to Action URL')
|
||||
->visible(fn($get) => $get('type') !== 'article'),
|
||||
|
||||
RichEditor::make('content')
|
||||
->visible(fn($get) => $get('type') !== 'image')
|
||||
->columnSpanFull()
|
||||
->hintAction(
|
||||
Action::make('picker')
|
||||
->label('Gallery Picker')
|
||||
->icon('heroicon-m-photo')
|
||||
->schema([
|
||||
Select::make('image_url')
|
||||
->label('Select an existing image')
|
||||
->allowHtml()
|
||||
->options(function () {
|
||||
return Media::latest()
|
||||
->limit(30) // Limit to 30 most recent items for performance
|
||||
->get(['id', 'file_name', 'name', 'uuid', 'collection_name', 'model_type', 'model_id', 'disk'])
|
||||
->filter(function (Media $item) {
|
||||
// Only include media items that have a valid disk
|
||||
return $item->disk !== null;
|
||||
})
|
||||
->mapWithKeys(function (Media $item) {
|
||||
try {
|
||||
$url = $item->getUrl();
|
||||
} catch (\Exception $e) {
|
||||
// Skip items that can't generate URLs
|
||||
return [];
|
||||
}
|
||||
|
||||
$fileName = e($item->file_name);
|
||||
$name = e($item->name ?? '');
|
||||
|
||||
// Smaller image preview for better performance
|
||||
$html = "<div class='flex items-center gap-3'>" .
|
||||
"<img src='{$url}' class='rounded' style='width:60px;height:60px;object-fit:cover;' alt='{$fileName}' loading='lazy' />" .
|
||||
"<div class='flex flex-col'>" .
|
||||
"<span class='font-medium text-sm'>{$name}</span>" .
|
||||
"<span class='text-xs text-gray-500'>{$fileName}</span>" .
|
||||
'</div></div>';
|
||||
|
||||
return [$url => $html];
|
||||
})->toArray();
|
||||
})
|
||||
->searchable()
|
||||
->preload()
|
||||
->required(),
|
||||
])
|
||||
->action(function (array $data, RichEditor $component) {
|
||||
// We dispatch the URL to the browser to be inserted into TipTap
|
||||
$component->getLivewire()->dispatch('insert-editor-content', [
|
||||
'statePath' => $component->getStatePath(),
|
||||
'html' => "<img src='{$data['image_url']}' alt=''>",
|
||||
]);
|
||||
})
|
||||
),
|
||||
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Entries\Schemas;
|
||||
|
||||
use Filament\Infolists\Components\IconEntry;
|
||||
use Filament\Infolists\Components\SpatieMediaLibraryImageEntry;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class EntryInfolist
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextEntry::make('title'),
|
||||
TextEntry::make('slug'),
|
||||
TextEntry::make('description')
|
||||
->placeholder('-')
|
||||
->columnSpanFull(),
|
||||
SpatieMediaLibraryImageEntry::make('featured_image')
|
||||
->collection('featured-image')
|
||||
->columnSpanFull(),
|
||||
IconEntry::make('is_published')
|
||||
->boolean(),
|
||||
IconEntry::make('is_featured')
|
||||
->boolean(),
|
||||
TextEntry::make('published_at')
|
||||
->date()
|
||||
->placeholder('-'),
|
||||
TextEntry::make('content')
|
||||
->placeholder('-')
|
||||
->columnSpanFull(),
|
||||
TextEntry::make('created_at')
|
||||
->dateTime()
|
||||
->placeholder('-'),
|
||||
TextEntry::make('updated_at')
|
||||
->dateTime()
|
||||
->placeholder('-'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Entries\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\SpatieMediaLibraryImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class EntriesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('id')
|
||||
->label('ID')
|
||||
->sortable(query: function ($query, $direction) {
|
||||
$query->orderBy('id', $direction);
|
||||
}),
|
||||
SpatieMediaLibraryImageColumn::make('featured_image')
|
||||
->collection('featured-image')
|
||||
->label('Image')
|
||||
->circular()
|
||||
->stacked()
|
||||
->limit(3),
|
||||
TextColumn::make('title')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('category.name')
|
||||
->label('Category')
|
||||
->sortable(query: function ($query, $direction) {
|
||||
$query->join('categories', 'entries.category_id', '=', 'categories.id')
|
||||
->orderBy('categories.name', $direction)
|
||||
->select('entries.*');
|
||||
})
|
||||
->searchable(),
|
||||
IconColumn::make('is_published')
|
||||
->label('pub')
|
||||
->boolean(),
|
||||
IconColumn::make('is_featured')
|
||||
->label('feat')
|
||||
->boolean(),
|
||||
TextColumn::make('published_at')
|
||||
->date()
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Media;
|
||||
|
||||
use App\Filament\Resources\Media\Pages\CreateMedia;
|
||||
use App\Filament\Resources\Media\Pages\EditMedia;
|
||||
use App\Filament\Resources\Media\Pages\ListMedia;
|
||||
use App\Filament\Resources\Media\Pages\ViewMedia;
|
||||
use App\Filament\Resources\Media\Schemas\MediaForm;
|
||||
use App\Filament\Resources\Media\Schemas\MediaInfolist;
|
||||
use App\Filament\Resources\Media\Tables\MediaTable;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
|
||||
class MediaResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Media::class;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'file_name';
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::Photo
|
||||
|
||||
;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return MediaForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function infolist(Schema $schema): Schema
|
||||
{
|
||||
return MediaInfolist::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return MediaTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListMedia::route('/'),
|
||||
'create' => CreateMedia::route('/create'),
|
||||
'view' => ViewMedia::route('/{record}'),
|
||||
'edit' => EditMedia::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Media\Pages;
|
||||
|
||||
use App\Filament\Resources\Media\MediaResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
|
||||
class CreateMedia extends CreateRecord
|
||||
{
|
||||
protected static string $resource = MediaResource::class;
|
||||
|
||||
protected ?string $uploadedFile = null;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
// Extract the file data before creating the media record
|
||||
$file = $data['file'] ?? null;
|
||||
unset($data['file']);
|
||||
|
||||
// Store file path for later
|
||||
$this->uploadedFile = $file;
|
||||
|
||||
// Set required fields for Media model
|
||||
$data['model_type'] = $data['model_type'] ?? 'temp';
|
||||
$data['model_id'] = $data['model_id'] ?? 0;
|
||||
$data['collection_name'] = $data['collection_name'] ?? 'default';
|
||||
$data['disk'] = $data['disk'] ?? 'public';
|
||||
$data['file_name'] = $file ? basename($file) : '';
|
||||
$data['mime_type'] = $file && Storage::disk('public')->exists($file)
|
||||
? Storage::disk('public')->mimeType($file)
|
||||
: 'application/octet-stream';
|
||||
$data['size'] = $file && Storage::disk('public')->exists($file)
|
||||
? Storage::disk('public')->size($file)
|
||||
: 0;
|
||||
$data['manipulations'] = [];
|
||||
$data['custom_properties'] = [];
|
||||
$data['generated_conversions'] = [];
|
||||
$data['responsive_images'] = [];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
if ($this->uploadedFile && $this->record) {
|
||||
$disk = Storage::disk('public');
|
||||
|
||||
// Create the directory for this media ID (Spatie structure: {id}/{filename})
|
||||
$mediaDirectory = (string) $this->record->id;
|
||||
$disk->makeDirectory($mediaDirectory);
|
||||
|
||||
// Move file from temporary upload location to Spatie's expected location
|
||||
if ($disk->exists($this->uploadedFile)) {
|
||||
$newPath = $mediaDirectory.'/'.$this->record->file_name;
|
||||
$disk->move($this->uploadedFile, $newPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Media\Pages;
|
||||
|
||||
use App\Filament\Resources\Media\MediaResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class EditMedia extends EditRecord
|
||||
{
|
||||
protected static string $resource = MediaResource::class;
|
||||
|
||||
protected ?string $uploadedFile = null;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
ViewAction::make(),
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
// Extract the file data if a new file was uploaded
|
||||
$file = $data['file'] ?? null;
|
||||
unset($data['file']);
|
||||
|
||||
// Only update file-related fields if a new file was uploaded
|
||||
if ($file && $file !== $this->record->getPathRelativeToRoot()) {
|
||||
$this->uploadedFile = $file;
|
||||
|
||||
// Keep the original file_name to prevent breaking existing references
|
||||
// $data['file_name'] is not updated - we preserve the original filename
|
||||
$data['mime_type'] = Storage::disk('public')->exists($file)
|
||||
? Storage::disk('public')->mimeType($file)
|
||||
: 'application/octet-stream';
|
||||
$data['size'] = Storage::disk('public')->exists($file)
|
||||
? Storage::disk('public')->size($file)
|
||||
: 0;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
if ($this->uploadedFile && $this->record) {
|
||||
$disk = Storage::disk('public');
|
||||
$mediaDirectory = (string) $this->record->id;
|
||||
|
||||
// Delete old file if it exists
|
||||
$oldPath = $mediaDirectory.'/'.$this->record->getOriginal('file_name');
|
||||
if ($disk->exists($oldPath)) {
|
||||
$disk->delete($oldPath);
|
||||
}
|
||||
|
||||
// Move new file to Spatie's expected location using the original filename
|
||||
if ($disk->exists($this->uploadedFile)) {
|
||||
$disk->makeDirectory($mediaDirectory);
|
||||
// Use the original file_name to preserve existing references
|
||||
$newPath = $mediaDirectory.'/'.$this->record->file_name;
|
||||
$disk->move($this->uploadedFile, $newPath);
|
||||
}
|
||||
|
||||
// Redirect to the same page to refresh the form state
|
||||
$this->redirect(static::getUrl(['record' => $this->record]), navigate: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Media\Pages;
|
||||
|
||||
use App\Filament\Resources\Media\MediaResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListMedia extends ListRecords
|
||||
{
|
||||
protected static string $resource = MediaResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
// CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Media\Pages;
|
||||
|
||||
use App\Filament\Resources\Media\MediaResource;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewMedia extends ViewRecord
|
||||
{
|
||||
protected static string $resource = MediaResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Media\Schemas;
|
||||
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media as SpatieMedia;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class MediaForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('collection_name')
|
||||
->default('default')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Hidden::make('disk')
|
||||
->default('public'),
|
||||
FileUpload::make('file')
|
||||
->multiple() // workaround for Filament v4 single-file bug
|
||||
->label('File')
|
||||
->imageEditor()
|
||||
->imageEditorAspectRatios([
|
||||
'16:9',
|
||||
'4:3',
|
||||
'1:1',
|
||||
])
|
||||
->columnSpanFull()
|
||||
->disk('s3')
|
||||
->directory('media')
|
||||
->visibility('public')
|
||||
->acceptedFileTypes(['image/*', 'application/pdf'])
|
||||
->maxSize(10240)
|
||||
->required(fn($context) => $context === 'create')
|
||||
|
||||
|
||||
->afterStateHydrated(function (FileUpload $component, $state, $record): void {
|
||||
Log::info('MediaForm afterStateHydrated invoked', ['record_id' => $record?->id, 'state' => $state]);
|
||||
|
||||
try {
|
||||
if (! $record) {
|
||||
return;
|
||||
}
|
||||
|
||||
$media = $record;
|
||||
if (! $media instanceof SpatieMedia) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct the correct path: {media_id}/{filename}
|
||||
$path = $media->id . '/' . $media->file_name;
|
||||
|
||||
try {
|
||||
$disk = $media->disk ?? 'public';
|
||||
if (Storage::disk($disk)->exists($path)) {
|
||||
$component->state($path);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('MediaForm afterStateHydrated storage check failed', [
|
||||
'err' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'disk' => $media->disk ?? null,
|
||||
'path' => $path,
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('MediaForm afterStateHydrated unhandled', [
|
||||
'err' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'record' => $record?->id,
|
||||
'state' => $state,
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}),
|
||||
|
||||
|
||||
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Media\Schemas;
|
||||
|
||||
use Filament\Infolists\Components\ImageEntry;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class MediaInfolist
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
ImageEntry::make('file_name')
|
||||
->label('Preview')
|
||||
->getStateUsing(fn ($record) => $record->getUrl())
|
||||
->visible(fn ($record) => $record->mime_type && str_starts_with($record->mime_type, 'image/')),
|
||||
TextEntry::make('name'),
|
||||
TextEntry::make('file_name'),
|
||||
TextEntry::make('mime_type'),
|
||||
TextEntry::make('collection_name'),
|
||||
TextEntry::make('size')
|
||||
->formatStateUsing(fn ($state) => number_format($state / 1024, 2).' KB'),
|
||||
TextEntry::make('model_type')
|
||||
->label('Attached to Model'),
|
||||
TextEntry::make('model_id'),
|
||||
TextEntry::make('custom_properties')
|
||||
->formatStateUsing(fn ($state) => json_encode($state, JSON_PRETTY_PRINT)),
|
||||
TextEntry::make('created_at')
|
||||
->dateTime(),
|
||||
TextEntry::make('updated_at')
|
||||
->dateTime(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Media\Tables;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
|
||||
class MediaTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->modifyQueryUsing(fn($query) => $query->where('collection_name', '!=', 'avatars'))
|
||||
->columns([
|
||||
ImageColumn::make('url')
|
||||
->label('Preview')
|
||||
->getStateUsing(
|
||||
fn($record) =>
|
||||
// Prefer the stored path produced by Filament's FileUpload (saved in custom_properties),
|
||||
// fall back to Spatie's getUrl() when no stored_path exists.
|
||||
($record->getCustomProperty('stored_path'))
|
||||
? Storage::url($record->getCustomProperty('stored_path'))
|
||||
: $record->getUrl()
|
||||
)
|
||||
->height(40)
|
||||
->width(40),
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('file_name')
|
||||
->searchable(),
|
||||
TextColumn::make('collection_name')
|
||||
->badge(),
|
||||
TextColumn::make('mime_type'),
|
||||
TextColumn::make('size')
|
||||
->formatStateUsing(fn($state) => number_format($state / 1024, 2) . ' KB'),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime(),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('collection_name')
|
||||
->options([
|
||||
'images' => 'Images',
|
||||
'documents' => 'Documents',
|
||||
]),
|
||||
])
|
||||
->recordActions([
|
||||
// EditAction::make(),
|
||||
DeleteAction::make()
|
||||
->action(function (Media $record) {
|
||||
// Delete the actual stored file path if we saved one, otherwise fall back to the Spatie path.
|
||||
$stored = $record->getCustomProperty('stored_path');
|
||||
if ($stored) {
|
||||
Storage::disk($record->disk)->delete($stored);
|
||||
} else {
|
||||
Storage::disk($record->disk)->delete($record->getPath());
|
||||
}
|
||||
|
||||
$record->delete();
|
||||
}),
|
||||
])
|
||||
->toolbarActions([
|
||||
|
||||
Action::make('add')
|
||||
->label('Add')
|
||||
->icon('heroicon-o-plus')
|
||||
->url(fn() => url('admin/assets/create'))
|
||||
->color('primary'),
|
||||
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->action(function (Collection $records) {
|
||||
$records->each(function (Media $record) {
|
||||
$stored = $record->getCustomProperty('stored_path');
|
||||
if ($stored) {
|
||||
Storage::disk($record->disk)->delete($stored);
|
||||
} else {
|
||||
Storage::disk($record->disk)->delete($record->getPath());
|
||||
}
|
||||
$record->delete();
|
||||
});
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\TextWidgets\Pages;
|
||||
|
||||
use App\Filament\Resources\TextWidgets\TextWidgetResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateTextWidget extends CreateRecord
|
||||
{
|
||||
protected static string $resource = TextWidgetResource::class;
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\TextWidgets\Pages;
|
||||
|
||||
use App\Filament\Resources\TextWidgets\TextWidgetResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditTextWidget extends EditRecord
|
||||
{
|
||||
protected static string $resource = TextWidgetResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\TextWidgets\Pages;
|
||||
|
||||
use App\Filament\Resources\TextWidgets\TextWidgetResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListTextWidgets extends ListRecords
|
||||
{
|
||||
protected static string $resource = TextWidgetResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\TextWidgets\Schemas;
|
||||
|
||||
use App\Models\Category;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class TextWidgetForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('title')
|
||||
->required()
|
||||
->live(onBlur: true),
|
||||
TextInput::make('description')
|
||||
->nullable(),
|
||||
Textarea::make('content')
|
||||
->rows(5)
|
||||
->columnSpanFull(),
|
||||
Select::make('category_id')
|
||||
->label('Category')
|
||||
->options(function () {
|
||||
return Category::all()
|
||||
->pluck('name', 'id')
|
||||
->toArray();
|
||||
})
|
||||
->searchable(),
|
||||
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\TextWidgets\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class TextWidgetsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('title')
|
||||
->label('Title')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
TextColumn::make('created_at')
|
||||
->label('Created')
|
||||
->dateTime('M d, Y H:i')
|
||||
->sortable(),
|
||||
TextColumn::make('updated_at')
|
||||
->label('Updated')
|
||||
->dateTime('M d, Y H:i')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\TextWidgets;
|
||||
|
||||
use App\Filament\Resources\TextWidgets\Pages\CreateTextWidget;
|
||||
use App\Filament\Resources\TextWidgets\Pages\EditTextWidget;
|
||||
use App\Filament\Resources\TextWidgets\Pages\ListTextWidgets;
|
||||
use App\Filament\Resources\TextWidgets\Schemas\TextWidgetForm;
|
||||
use App\Filament\Resources\TextWidgets\Tables\TextWidgetsTable;
|
||||
use App\Models\TextWidget;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class TextWidgetResource extends Resource
|
||||
{
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
protected static ?string $model = TextWidget::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::DocumentText;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'title';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return TextWidgetForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return TextWidgetsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListTextWidgets::route('/'),
|
||||
'create' => CreateTextWidget::route('/create'),
|
||||
'edit' => EditTextWidget::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreCategoryRequest;
|
||||
use App\Http\Requests\UpdateCategoryRequest;
|
||||
use App\Http\Resources\CategoryResource;
|
||||
use App\Models\Category;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return CategoryResource::collection(Category::all());
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(StoreCategoryRequest $request)
|
||||
{
|
||||
return new CategoryResource(Category::create($request->validated()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Category $category)
|
||||
{
|
||||
return new CategoryResource($category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(UpdateCategoryRequest $request, Category $category)
|
||||
{
|
||||
$category->update($request->validated());
|
||||
|
||||
return new CategoryResource($category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Category $category)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user || $user->email !== config('app.admin_email')) {
|
||||
return response()->json(['message' => 'Forbidden'], 403);
|
||||
}
|
||||
|
||||
$category->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Resources\EntryResource;
|
||||
use App\Models\Entry;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class EntryController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return EntryResource::collection(Entry::with('category')->get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if (! $user || $user->email !== config('app.admin_email')) {
|
||||
return response()->json(['message' => 'Forbidden'], 403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'content' => 'required|string',
|
||||
]);
|
||||
|
||||
$validated['slug'] = $this->generateUniqueSlug($validated['title']);
|
||||
|
||||
return new EntryResource(Entry::create($validated));
|
||||
}
|
||||
|
||||
private function generateUniqueSlug(string $title): string
|
||||
{
|
||||
do {
|
||||
$slug = Str::slug($title).'-'.Str::random(8);
|
||||
} while (Entry::where('slug', $slug)->exists());
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Entry $entry)
|
||||
{
|
||||
$this->authorize('view', $entry);
|
||||
|
||||
return new EntryResource($entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Entry $entry)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'sometimes|required|string|max:255',
|
||||
'content' => 'sometimes|required|string',
|
||||
]);
|
||||
|
||||
$entry->update($validated);
|
||||
|
||||
return new EntryResource($entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Entry $entry)
|
||||
{
|
||||
$entry->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return Auth::check();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Events\PreviewSiteBuilt;
|
||||
use App\Http\Requests\SendPreviewSiteBuiltNotificationRequest;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class NotificationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Send preview site built notification.
|
||||
*/
|
||||
public function sendPreviewSiteBuilt(SendPreviewSiteBuiltNotificationRequest $request): JsonResponse
|
||||
{
|
||||
$message = $request->validated()['message'];
|
||||
|
||||
PreviewSiteBuilt::dispatch($message, 'success');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Preview site built notification sent successfully',
|
||||
'data' => [
|
||||
'notification_message' => $message,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Resources\TextWidgetResource;
|
||||
use App\Models\TextWidget;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class TextWidgetController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return response()->json([
|
||||
'data' => TextWidget::with('category')->get()->map(fn ($tw) => new TextWidgetResource($tw))
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if (! $user || $user->email !== config('app.admin_email')) {
|
||||
return response()->json(['message' => 'Forbidden'], 403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'content' => 'required|string',
|
||||
]);
|
||||
|
||||
return new TextWidgetResource(TextWidget::create($validated));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(TextWidget $textWidget)
|
||||
{
|
||||
return new TextWidgetResource($textWidget->load('category'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, TextWidget $textWidget)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'sometimes|required|string|max:255',
|
||||
'description' => 'sometimes|nullable|string',
|
||||
'content' => 'sometimes|required|string',
|
||||
]);
|
||||
|
||||
$textWidget->update($validated);
|
||||
|
||||
return new TextWidgetResource($textWidget->load('category'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(TextWidget $textWidget)
|
||||
{
|
||||
$textWidget->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SendPreviewSiteBuiltNotificationRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() && $this->user()->email === config('app.admin_email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'message' => 'required|string|max:255',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreCategoryRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() && $this->user()->email === config('app.admin_email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255|unique:categories,name',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreEntryRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() && $this->user()->email === config('app.admin_email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'required|string|max:255',
|
||||
'content' => 'required|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateCategoryRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() && $this->user()->email === config('app.admin_email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'sometimes|required|string|max:255|unique:categories,name,'.$this->category->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateEntryRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() && $this->user()->email === config('app.admin_email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'sometimes|required|string|max:255',
|
||||
'content' => 'sometimes|required|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class CategoryResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class EntryResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'slug' => $this->slug,
|
||||
'description' => $this->description,
|
||||
'is_published' => $this->is_published,
|
||||
'is_featured' => $this->is_featured,
|
||||
'published_at' => $this->published_at,
|
||||
'content' => $this->content,
|
||||
'category' => $this->category->name ?? null,
|
||||
'featured_image_url' => $this->getFirstMediaUrl('featured-image') ?: null,
|
||||
'call_to_action_text' => $this->call_to_action_text,
|
||||
'call_to_action_link' => $this->call_to_action_link,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
'priority' => $this->priority,
|
||||
'type' => $this->type,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class TextWidgetResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'description' => $this->description,
|
||||
'content' => $this->content,
|
||||
'category' => $this->category->name ?? null,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Services\ProcessUpdateService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProcessChangeUpdate implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public int $changeId,
|
||||
public string $action,
|
||||
public string $type = 'change_update_'
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
Log::info("Processing change update: changeId={$this->changeId}, action={$this->action}");
|
||||
(new ProcessUpdateService)->processUpdate($this->changeId, $this->action, $this->type);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Services\ProcessUpdateService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ProcessEntryUpdate implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public int $entryId,
|
||||
public string $action,
|
||||
public string $type = 'entry_update_'
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
(new ProcessUpdateService)->processUpdate($this->entryId, $this->action, $this->type);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
|
||||
class GalleryPicker extends Component
|
||||
{
|
||||
#[\Livewire\Attributes\Reactive]
|
||||
public $entryId;
|
||||
|
||||
public $mediaItems = [];
|
||||
public $selectedMediaId = null;
|
||||
public $showModal = false;
|
||||
|
||||
#[\Livewire\Attributes\On('open-gallery-picker')]
|
||||
public function openPicker($entryId): void
|
||||
{
|
||||
$this->entryId = $entryId;
|
||||
$this->loadMediaItems();
|
||||
$this->showModal = true;
|
||||
}
|
||||
|
||||
public function loadMediaItems(): void
|
||||
{
|
||||
$this->mediaItems = Media::where('model_type', 'temp')
|
||||
->where('model_id', 0)
|
||||
->where('disk', 'public')
|
||||
->latest()
|
||||
->limit(30)
|
||||
->get(['id', 'file_name', 'name', 'disk'])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function selectMedia($mediaId): void
|
||||
{
|
||||
$this->selectedMediaId = $mediaId;
|
||||
}
|
||||
|
||||
public function copyToEntry(): void
|
||||
{
|
||||
if (!$this->selectedMediaId || !$this->entryId) {
|
||||
$this->dispatch('notify-error', ['message' => 'Please select an image']);
|
||||
return;
|
||||
}
|
||||
|
||||
$sourceMedia = Media::find($this->selectedMediaId);
|
||||
if (!$sourceMedia) {
|
||||
$this->dispatch('notify-error', ['message' => 'Media not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the entry
|
||||
$entry = \App\Models\Entry::find($this->entryId);
|
||||
if (!$entry) {
|
||||
$this->dispatch('notify-error', ['message' => 'Entry not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get source file
|
||||
$sourceFile = $sourceMedia->getPath();
|
||||
if (!file_exists($sourceFile)) {
|
||||
$this->dispatch('notify-error', ['message' => 'Source file not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create temp copy
|
||||
$tempCopy = sys_get_temp_dir() . '/' . uniqid() . '_' . $sourceMedia->file_name;
|
||||
copy($sourceFile, $tempCopy);
|
||||
|
||||
try {
|
||||
// Clear existing featured image
|
||||
$entry->clearMediaCollection('featured-image');
|
||||
|
||||
// Add to entry
|
||||
$newMedia = $entry->addMedia($tempCopy)
|
||||
->usingName($sourceMedia->name ?: pathinfo($sourceMedia->file_name, PATHINFO_FILENAME))
|
||||
->usingFileName($sourceMedia->file_name)
|
||||
->toMediaCollection('featured-image', 'public');
|
||||
|
||||
// Close modal and notify
|
||||
$this->showModal = false;
|
||||
$this->selectedMediaId = null;
|
||||
$this->dispatch('media-selected', ['mediaId' => $newMedia->id, 'fileName' => $newMedia->file_name]);
|
||||
$this->dispatch('notify-success', ['message' => 'Image added to entry']);
|
||||
} finally {
|
||||
if (file_exists($tempCopy)) {
|
||||
unlink($tempCopy);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatch('notify-error', ['message' => 'Error: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function closePicker(): void
|
||||
{
|
||||
$this->showModal = false;
|
||||
$this->selectedMediaId = null;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.gallery-picker');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
|
||||
class Asset extends Model implements HasMedia
|
||||
{
|
||||
use InteractsWithMedia;
|
||||
|
||||
protected $fillable = [
|
||||
'alt_text',
|
||||
];
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Category extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['name'];
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Change extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'note',
|
||||
'user_id',
|
||||
'type',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Filament\Forms\Components\RichEditor\FileAttachmentProviders\SpatieMediaLibraryFileAttachmentProvider;
|
||||
use Filament\Forms\Components\RichEditor\Models\Concerns\InteractsWithRichContent;
|
||||
use Filament\Forms\Components\RichEditor\Models\Contracts\HasRichContent;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
use Spatie\Tags\HasTags;
|
||||
|
||||
/*
|
||||
Entry model with rich content and media library integration
|
||||
This is the main article / blog rich content model
|
||||
*/
|
||||
|
||||
class Entry extends Model implements HasMedia, HasRichContent
|
||||
{
|
||||
use HasFactory, HasTags, InteractsWithMedia, InteractsWithRichContent;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'type',
|
||||
'slug',
|
||||
'description',
|
||||
'is_published',
|
||||
'is_featured',
|
||||
'published_at',
|
||||
'content',
|
||||
'call_to_action_link',
|
||||
'call_to_action_text',
|
||||
'category_id',
|
||||
'priority',
|
||||
];
|
||||
|
||||
/**
|
||||
* Set up rich content configuration for media library integration
|
||||
*/
|
||||
public function setUpRichContent(): void
|
||||
{
|
||||
$this->registerRichContent('content')
|
||||
->fileAttachmentProvider(
|
||||
SpatieMediaLibraryFileAttachmentProvider::make()
|
||||
->collection('content-attachments')
|
||||
->preserveFilenames()
|
||||
);
|
||||
}
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($entry) {
|
||||
if (empty($entry->slug)) {
|
||||
$entry->slug = Str::slug($entry->title);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class TextWidget extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'description',
|
||||
'content',
|
||||
'category_id',
|
||||
];
|
||||
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,20 +3,16 @@
|
|||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
|
||||
use Filament\Models\Contracts\FilamentUser;
|
||||
use Filament\Panel;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class User extends Authenticatable implements FilamentUser
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
|
||||
use HasFactory, Notifiable, TwoFactorAuthenticatable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
|
@ -65,17 +61,4 @@ class User extends Authenticatable implements FilamentUser
|
|||
->map(fn ($word) => Str::substr($word, 0, 1))
|
||||
->implode('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user can access Filament admin panel.
|
||||
*/
|
||||
public function canAccessPanel(Panel $panel): bool
|
||||
{
|
||||
return $this->email === config('app.admin_email') || $this->role === 'admin';
|
||||
}
|
||||
|
||||
public function changes()
|
||||
{
|
||||
return $this->hasMany(Change::class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Jobs\ProcessChangeUpdate;
|
||||
use App\Models\Change;
|
||||
|
||||
class ChangeObserver
|
||||
{
|
||||
/**
|
||||
* Handle the Change "created" event.
|
||||
*/
|
||||
public function created(Change $change): void
|
||||
{
|
||||
ProcessChangeUpdate::dispatch($change->id, 'created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Change "updated" event.
|
||||
*/
|
||||
public function updated(Change $change): void
|
||||
{
|
||||
ProcessChangeUpdate::dispatch($change->id, 'updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Change "deleted" event.
|
||||
*/
|
||||
public function deleted(Change $change): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Change "restored" event.
|
||||
*/
|
||||
public function restored(Change $change): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Change "force deletecURLd" event.
|
||||
*/
|
||||
public function forceDeleted(Change $change): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Jobs\ProcessEntryUpdate;
|
||||
use App\Models\Entry;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class EntryObserver
|
||||
{
|
||||
/**
|
||||
* Handle the Entry "created" event.
|
||||
*/
|
||||
public function created(Entry $entry): void
|
||||
{
|
||||
ProcessEntryUpdate::dispatch($entry->id, 'created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Entry "updated" event.
|
||||
*/
|
||||
public function updated(Entry $entry): void
|
||||
{
|
||||
ProcessEntryUpdate::dispatch($entry->id, 'updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Entry "deleted" event.
|
||||
*/
|
||||
public function deleted(Entry $entry): void
|
||||
{
|
||||
ProcessEntryUpdate::dispatch($entry->id, 'deleted');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Entry "restored" event.
|
||||
*/
|
||||
public function restored(Entry $entry): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Entry "force deleted" event.
|
||||
*/
|
||||
public function forceDeleted(Entry $entry): void
|
||||
{
|
||||
// ProcessEntryUpdate::dispatch($entry, 'force deleted');
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,7 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Entry;
|
||||
use App\Observers\EntryObserver;
|
||||
use App\Models\Change;
|
||||
use App\Observers\ChangeObserver;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
|
@ -24,13 +19,6 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
// Ensure the livewire-tmp directory exists
|
||||
$livewireTmpPath = storage_path('framework/livewire-tmp');
|
||||
if (!File::exists($livewireTmpPath)) {
|
||||
File::makeDirectory($livewireTmpPath, 0755, true);
|
||||
}
|
||||
|
||||
Entry::observe(EntryObserver::class);
|
||||
Change::observe(ChangeObserver::class);
|
||||
//
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ use Filament\Pages\Dashboard;
|
|||
use Filament\Panel;
|
||||
use Filament\PanelProvider;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Filament\Support\Facades\FilamentView;
|
||||
use Filament\View\PanelsRenderHook;
|
||||
use Filament\Widgets\AccountWidget;
|
||||
use Filament\Widgets\FilamentInfoWidget;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
|
|
@ -25,38 +23,13 @@ class AdminPanelProvider extends PanelProvider
|
|||
{
|
||||
public function panel(Panel $panel): Panel
|
||||
{
|
||||
$previewSiteUrl = env('PREVIEW_SITE_URL');
|
||||
$liveSiteUrl = env('LIVE_SITE_URL');
|
||||
|
||||
$navigationItems = [];
|
||||
if ($previewSiteUrl && $liveSiteUrl) {
|
||||
$navigationItems = [
|
||||
\Filament\Navigation\NavigationItem::make('Preview Site')
|
||||
->url($previewSiteUrl, shouldOpenInNewTab: true)
|
||||
->icon('heroicon-o-eye')
|
||||
->group('External Links')
|
||||
->sort(1),
|
||||
\Filament\Navigation\NavigationItem::make('Live Site')
|
||||
->url($liveSiteUrl, shouldOpenInNewTab: true)
|
||||
->icon('heroicon-o-rocket-launch')
|
||||
->group('External Links')
|
||||
->sort(2),
|
||||
];
|
||||
}
|
||||
|
||||
return $panel
|
||||
->default()
|
||||
->sidebarCollapsibleOnDesktop()
|
||||
->id('admin')
|
||||
->path('admin')
|
||||
->login()
|
||||
->colors([
|
||||
'primary' => Color::Blue,
|
||||
])
|
||||
->resources([
|
||||
\App\Filament\Resources\Entries\EntryResource::class,
|
||||
\App\Filament\Resources\Media\MediaResource::class,
|
||||
\App\Filament\Resources\Categroys\CategroyResource::class,
|
||||
'primary' => Color::Amber,
|
||||
])
|
||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
|
||||
->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages')
|
||||
|
|
@ -68,7 +41,6 @@ class AdminPanelProvider extends PanelProvider
|
|||
AccountWidget::class,
|
||||
FilamentInfoWidget::class,
|
||||
])
|
||||
->navigationItems($navigationItems)
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
|
|
@ -84,151 +56,4 @@ class AdminPanelProvider extends PanelProvider
|
|||
Authenticate::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
FilamentView::registerRenderHook(
|
||||
PanelsRenderHook::BODY_END,
|
||||
fn(): string => \Illuminate\Support\Facades\Blade::render('@vite("resources/js/app.js")'),
|
||||
);
|
||||
|
||||
FilamentView::registerRenderHook(
|
||||
PanelsRenderHook::BODY_END,
|
||||
function (): string {
|
||||
return '
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
if (window.Echo) {
|
||||
console.log("Setting up Filament Reverb notifications...");
|
||||
console.log("Available globals:", {
|
||||
FilamentNotification: typeof FilamentNotification,
|
||||
Livewire: typeof window.Livewire,
|
||||
filament: typeof window.filament
|
||||
});
|
||||
|
||||
// Listen for preview site built events
|
||||
window.Echo.channel("filament-notifications")
|
||||
.listen("preview-site.built", function(event) {
|
||||
console.log("🎉 Received preview site built event:", event);
|
||||
|
||||
// Use Filament v4 notification system
|
||||
if (typeof FilamentNotification !== "undefined") {
|
||||
new FilamentNotification()
|
||||
.title(event.message)
|
||||
.success()
|
||||
.duration(5000)
|
||||
.send();
|
||||
console.log("✅ Sent via FilamentNotification v4");
|
||||
} else {
|
||||
console.warn("FilamentNotification not available");
|
||||
// Fallback notification
|
||||
const notification = document.createElement("div");
|
||||
notification.style.cssText = `
|
||||
position: fixed !important;
|
||||
top: 20px !important;
|
||||
right: 20px !important;
|
||||
z-index: 999999 !important;
|
||||
background: #10b981 !important;
|
||||
color: white !important;
|
||||
padding: 16px 20px !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.3) !important;
|
||||
font-family: system-ui, -apple-system, sans-serif !important;
|
||||
font-size: 14px !important;
|
||||
font-weight: 500 !important;
|
||||
max-width: 350px !important;
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
`;
|
||||
|
||||
notification.innerHTML = `
|
||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<div style="margin-right: 15px;">
|
||||
✓ ${event.message}
|
||||
</div>
|
||||
<button onclick="this.parentElement.parentElement.remove()"
|
||||
style="background: none; border: none; color: white; font-size: 18px; cursor: pointer; padding: 0;">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
console.log("✅ Created fallback notification");
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
})
|
||||
.listen(".preview-site.built", function(event) {
|
||||
console.log("🔄 Also listening with dot prefix (the working one):", event);
|
||||
console.log("message:", event.message);
|
||||
|
||||
// Use Filament v4 notification system
|
||||
if (typeof FilamentNotification !== "undefined") {
|
||||
new FilamentNotification()
|
||||
.title(event.message)
|
||||
.success()
|
||||
.duration(5000)
|
||||
.send();
|
||||
console.log("✅ Sent via FilamentNotification v4 (from dot prefix)");
|
||||
} else {
|
||||
console.warn("FilamentNotification not available, creating fallback");
|
||||
// Fallback notification
|
||||
const notification = document.createElement("div");
|
||||
notification.style.cssText = `
|
||||
position: fixed !important;
|
||||
top: 20px !important;
|
||||
right: 20px !important;
|
||||
z-index: 999999 !important;
|
||||
background: #3b82f6 !important;
|
||||
color: white !important;
|
||||
padding: 16px 20px !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.3) !important;
|
||||
font-family: system-ui, -apple-system, sans-serif !important;
|
||||
font-size: 14px !important;
|
||||
font-weight: 500 !important;
|
||||
max-width: 350px !important;
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
`;
|
||||
|
||||
notification.innerHTML = `
|
||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<div style="margin-right: 15px;">
|
||||
🔄 ${event.message}
|
||||
</div>
|
||||
<button onclick="this.parentElement.parentElement.remove()"
|
||||
style="background: none; border: none; color: white; font-size: 18px; cursor: pointer; padding: 0;">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
console.log("✅ Created dot prefix fallback notification");
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("✅ Event listeners set up for filament-notifications channel");
|
||||
} else {
|
||||
console.error("❌ Echo is not available for Filament notifications");
|
||||
}
|
||||
});
|
||||
</script>';
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
|
||||
class ProcessUpdateService
|
||||
{
|
||||
public function processUpdate(int $entryId, string $action, string $type = 'entry_update_'): void
|
||||
{
|
||||
$subject = env('NATS_SUBJECT', 'entry_update');
|
||||
$appUrl = env('APP_URL', 'http://localhost');
|
||||
|
||||
$incoming = [
|
||||
'id' => $entryId,
|
||||
'action' => $action,
|
||||
'subject' => $subject,
|
||||
'app_url' => $appUrl,
|
||||
'type' => $type,
|
||||
];
|
||||
|
||||
$jsonData = json_encode($incoming, JSON_THROW_ON_ERROR);
|
||||
$tempFile = tempnam(sys_get_temp_dir(), $type);
|
||||
file_put_contents($tempFile, $jsonData);
|
||||
|
||||
$scriptPath = env('HANDLE_ENTRY_UPDATES_SCRIPT', base_path('cmd/handle_cms_updates.sh'));
|
||||
|
||||
try {
|
||||
// Log what we're about to execute
|
||||
Log::info("Executing script: {$scriptPath} with action: {$action}", [
|
||||
'entry_id' => $entryId,
|
||||
'temp_file' => $tempFile,
|
||||
'script_exists' => file_exists($scriptPath),
|
||||
'script_executable' => is_executable($scriptPath),
|
||||
]);
|
||||
|
||||
$result = Process::run([
|
||||
'bash',
|
||||
$scriptPath,
|
||||
$action,
|
||||
$tempFile,
|
||||
]);
|
||||
|
||||
if ($result->failed()) {
|
||||
$errorDetails = [
|
||||
'exit_code' => $result->exitCode(),
|
||||
'stdout' => $result->output(),
|
||||
'stderr' => $result->errorOutput(),
|
||||
'command' => ['bash', $scriptPath, $action, $tempFile],
|
||||
'script_path' => $scriptPath,
|
||||
'script_exists' => file_exists($scriptPath),
|
||||
'temp_file_exists' => file_exists($tempFile),
|
||||
'temp_file_contents' => file_exists($tempFile) ? file_get_contents($tempFile) : 'N/A',
|
||||
];
|
||||
|
||||
Log::error('Script execution failed', $errorDetails);
|
||||
|
||||
throw new RuntimeException(
|
||||
"Script execution failed with exit code {$result->exitCode()}. " .
|
||||
'STDOUT: ' . ($result->output() ?: 'empty') . ' ' .
|
||||
'STDERR: ' . ($result->errorOutput() ?: 'empty')
|
||||
);
|
||||
}
|
||||
|
||||
Log::info('Script executed successfully', [
|
||||
'stdout' => $result->output(),
|
||||
'entry_id' => $entryId,
|
||||
'action' => $action,
|
||||
]);
|
||||
} finally {
|
||||
// Clean up temp file
|
||||
if (file_exists($tempFile)) {
|
||||
unlink($tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,9 +7,7 @@ use Illuminate\Foundation\Configuration\Middleware;
|
|||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
channels: __DIR__.'/../routes/channels.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
#SQLITE_DB_PATH="../share-lt/database/database.sqlite"
|
||||
SQLITE_DB_PATH="../share-lt/database/database.sqlite.backup5.recovery"
|
||||
BACKUP_DIR="database/backups"
|
||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||
BACKUP_FILE="$BACKUP_DIR/backup_$TIMESTAMP.sql"
|
||||
TABLES="taggables media tags categories text_widgets entries users"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
for TABLE in $TABLES; do
|
||||
echo "Backing up table: $TABLE"
|
||||
sqlite3 "$SQLITE_DB_PATH" ".dump $TABLE" > "$BACKUP_DIR/${TABLE}_backup_$TIMESTAMP.sql"
|
||||
done
|
||||
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
LARAVEL_CONTAINER_NAME="quay.io/marshyon/share-lt"
|
||||
CONTAINER_LABEL="0.0.7"
|
||||
CACHE="--no-cache"
|
||||
CACHE=""
|
||||
|
||||
docker build \
|
||||
$CACHE \
|
||||
-t ${LARAVEL_CONTAINER_NAME}:${CONTAINER_LABEL} \
|
||||
-f Dockerfile .
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
LARAVEL_CONTAINER_NAME="quay.io/marshyon/share-lt"
|
||||
CONTAINER_LABEL="v0.0.8"
|
||||
CACHE="--no-cache"
|
||||
# CACHE=""
|
||||
|
||||
docker build \
|
||||
$CACHE \
|
||||
-t ${LARAVEL_CONTAINER_NAME}:${CONTAINER_LABEL} .
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# TOKEN="your_api_token_here"
|
||||
# ensure to have set TOKEN to a valid value before running
|
||||
# ideally add this to an .envrc file and source it
|
||||
# tokens need to be created with tinker or similar method
|
||||
|
||||
|
||||
URL='http://127.0.0.1:8000/api/categories'
|
||||
|
||||
curl -s -X GET \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
$URL
|
||||
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# TOKEN="your_api_token_here"
|
||||
# ensure to have set TOKEN to a valid value before running
|
||||
# ideally add this to an .envrc file and source it
|
||||
# tokens need to be created with tinker or similar method
|
||||
|
||||
|
||||
URL='http://127.0.0.1:8000/api/categories'
|
||||
|
||||
curl -s -X GET \
|
||||
-H "Accept: application/json" \
|
||||
$URL
|
||||
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# TOKEN="your_api_token_here"
|
||||
# ensure to have set TOKEN to a valid value before running
|
||||
# ideally add this to an .envrc file and source it
|
||||
# tokens need to be created with tinker or similar method
|
||||
|
||||
|
||||
URL="${ADDRESS:-http://127.0.0.1:8000/api/entries}"
|
||||
curl -s -X GET \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
$URL
|
||||
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# this should fail, no token provided
|
||||
# users need to be authenticated and have been
|
||||
# granted access to view entries by being given
|
||||
# a token
|
||||
|
||||
URL="${ADDRESS:-http://127.0.0.1:8000/api/entries}"
|
||||
curl -s -X GET \
|
||||
-H "Accept: application/json" \
|
||||
$URL
|
||||
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# TOKEN="your_api_token_here"
|
||||
# ensure to have set TOKEN to a valid value before running
|
||||
# ideally add this to an .envrc file and source it
|
||||
# tokens need to be created with tinker or similar method
|
||||
|
||||
|
||||
URL='http://127.0.0.1:8000/api/text-widgets'
|
||||
|
||||
curl -s -X GET \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
$URL
|
||||
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# this should fail, no token provided
|
||||
# users need to be authenticated and have been
|
||||
# granted access to view text widgets by being given
|
||||
# a token
|
||||
|
||||
URL='http://127.0.0.1:8000/api/text-widgets'
|
||||
|
||||
curl -s -X GET \
|
||||
-H "Accept: application/json" \
|
||||
$URL
|
||||
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# TOKEN="your_api_token_here"
|
||||
# ensure to have set TOKEN to a valid value before running
|
||||
# ideally add this to an .envrc file and source it
|
||||
# only the admin user can create entries so this should
|
||||
# fail unless .env has ADMIN_EMAIL set to the user that
|
||||
# the token belongs to
|
||||
|
||||
URL='http://127.0.0.1:8000/api/entries'
|
||||
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"title": "Yet Another New Entry Title",
|
||||
"content": "This is the content yet again of the new entry."
|
||||
}' \
|
||||
$URL
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# this should fail as no token is provided
|
||||
# user is not authenticated
|
||||
# no token has been granted
|
||||
|
||||
URL='http://127.0.0.1:8000/api/entries'
|
||||
|
||||
curl -X POST \
|
||||
-H "Accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"title": "Yet Another New Entry Title",
|
||||
"content": "This is the content yet again of the new entry."
|
||||
}' \
|
||||
$URL
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# TOKEN="your_api_token_here"
|
||||
# ensure to have set TOKEN to a valid value before running
|
||||
# ideally add this to an .envrc file and source it
|
||||
# only the admin user can create entries so this should
|
||||
# fail unless .env has ADMIN_EMAIL set to the user that
|
||||
# the token belongs to
|
||||
|
||||
URL='http://127.0.0.1:8000/api/text-widgets'
|
||||
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"title": "Yet Another New text widget Title",
|
||||
"content": "This is the content yet again of the new text widget."
|
||||
}' \
|
||||
$URL
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# this should fail as no token is provided
|
||||
# user is not authenticated
|
||||
# no token has been granted
|
||||
|
||||
URL='http://127.0.0.1:8000/api/text-widgets'
|
||||
|
||||
curl -X POST \
|
||||
-H "Accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"title": "Yet Another New Entry Title",
|
||||
"content": "This is the content yet again of the new entry."
|
||||
}' \
|
||||
$URL
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# URL='http://127.0.0.1:8000'
|
||||
|
||||
curl -X POST $URL/api/notifications/preview-site-built \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message": "Published to LIVE 💯🚀🎯 - site notification!"}'
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Ensure APP_KEY is set and persisted
|
||||
PERSISTED_KEY="/var/www/storage/.app_key"
|
||||
|
||||
if [ -z "$APP_KEY" ]; then
|
||||
if [ -f "$PERSISTED_KEY" ]; then
|
||||
echo "Using persisted APP_KEY from: $PERSISTED_KEY"
|
||||
export APP_KEY=$(cat "$PERSISTED_KEY")
|
||||
else
|
||||
# Generate key, strip "base64:", and save
|
||||
NEW_KEY=$(php artisan key:generate --show --no-interaction)
|
||||
echo "Generated new APP_KEY to: $PERSISTED_KEY"
|
||||
echo "$NEW_KEY" > "$PERSISTED_KEY"
|
||||
export APP_KEY="$NEW_KEY"
|
||||
fi
|
||||
fi
|
||||
|
||||
# check to see if /var/www/database/database.sqlite exists
|
||||
# if not, run migrations
|
||||
if [ ! -f /var/www/database/database.sqlite ]; then
|
||||
php artisan migrate --force
|
||||
fi
|
||||
|
||||
# check to see if /var/www/public/storage exists
|
||||
# if not, run storage:link
|
||||
if [ ! -d /var/www/public/storage ]; then
|
||||
php artisan storage:link
|
||||
fi
|
||||
|
||||
php artisan config:clear
|
||||
npm run build
|
||||
|
||||
# Start supervisord directly
|
||||
exec "$@"
|
||||
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
LOG_FILE="/tmp/logfile.log"
|
||||
|
||||
# Redirect all output to both stdout and the log file
|
||||
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||
|
||||
# Read the first two command line arguments
|
||||
ACTION=$1
|
||||
FILENAME=$2
|
||||
|
||||
# Check if the file exists and echo its contents
|
||||
if [[ -f "$FILENAME" ]]; then
|
||||
echo "Contents of the file $FILENAME:"
|
||||
cat "$FILENAME" | jq
|
||||
else
|
||||
echo "Error: File $FILENAME does not exist."
|
||||
fi
|
||||
|
||||
echo
|
||||
# Read and print command line arguments
|
||||
echo "=============================="
|
||||
echo "ACTION: $ACTION"
|
||||
echo "FILENAME: $FILENAME"
|
||||
echo "=============================="
|
||||
echo
|
||||
|
||||
# Publish message and check return code
|
||||
if nats pub $NATS_SUBJECT "$(cat "$FILENAME")" --server $NATS_URL --user $NATS_USERNAME --password $NATS_PASSWORD; then
|
||||
echo "Success: Message published to NATS successfully."
|
||||
else
|
||||
echo "Error: Failed to publish message to NATS."
|
||||
fi
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue