Compare commits
24 commits
dev
...
feat/spati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22393b5954 | ||
|
|
56ce59fc22 | ||
|
|
cfb9353475 | ||
|
|
b0fc008530 | ||
|
|
33688b55be | ||
|
|
50e5fb7f3f | ||
|
|
ae662a30ef | ||
|
|
bef3ae7f41 | ||
|
|
4cb9d078b1 | ||
|
|
1e35e485ad | ||
|
|
ff5ab3aa58 | ||
|
|
aa39707e10 | ||
|
|
9f8c8d43f5 | ||
|
|
93c977d1f5 | ||
|
|
8e1650653b | ||
|
|
6d1d88542e | ||
|
|
a94a34ce3b | ||
|
|
c49249ee20 | ||
|
|
340b466ade | ||
|
|
9f01d44c9d | ||
|
|
d24b9b0732 | ||
|
|
02884d4e2b | ||
|
|
d40b87438d | ||
|
|
5ea0ddce23 |
178 changed files with 879 additions and 7017 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}"
|
||||
|
|
|
|||
18
.github/copilot-instructions.md
vendored
18
.github/copilot-instructions.md
vendored
|
|
@ -13,8 +13,6 @@ 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
|
||||
|
|
@ -46,6 +44,13 @@ This application is a Laravel application and its main Laravel ecosystems packag
|
|||
## Documentation Files
|
||||
- You must only create documentation files if explicitly requested by the user.
|
||||
|
||||
## Testing Approach - CRITICAL
|
||||
- **THE APPLICATION IS 100% WORKING** - All functionality works perfectly in production.
|
||||
- When writing or debugging browser tests (Laravel Dusk), focus ONLY on test syntax, selectors, and Dusk interaction methods.
|
||||
- NEVER assume the application has bugs or suggest app fixes - the issue is always in the test code.
|
||||
- Trust the existing functionality and work on getting the correct CSS selectors, XPath expressions, and Dusk methods.
|
||||
- If manual interaction works but the test fails, the problem is the test implementation, not the app.
|
||||
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
|
|
@ -500,12 +505,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.
|
||||
|
|
|
|||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -9,7 +9,6 @@
|
|||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.env.dev
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
|
|
@ -25,10 +24,3 @@ yarn-error.log
|
|||
.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"
|
||||
|
||||
59
CHANGELOG.md
59
CHANGELOG.md
|
|
@ -1,67 +1,12 @@
|
|||
|
||||
# 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
|
||||
|
||||
## 2026-01-02
|
||||
|
||||
added initial model and filament resource
|
||||
|
||||
|
|
|
|||
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,8 +13,6 @@ 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
|
||||
|
|
|
|||
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');
|
||||
}
|
||||
}
|
||||
112
app/Console/Commands/MoveMediaToPublic.php
Normal file
112
app/Console/Commands/MoveMediaToPublic.php
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
|
||||
class MoveMediaToPublic extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'media:move-to-public {--dry-run : Show what would be moved without actually moving files}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Move all media files from private/local disk to public disk';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn('DRY RUN MODE - No files will actually be moved');
|
||||
}
|
||||
|
||||
// Get all media records using local disk
|
||||
$mediaRecords = Media::where('disk', 'local')->get();
|
||||
|
||||
if ($mediaRecords->isEmpty()) {
|
||||
$this->info('No media records found using local disk.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Found {$mediaRecords->count()} media records to migrate.");
|
||||
|
||||
$progressBar = $this->output->createProgressBar($mediaRecords->count());
|
||||
$progressBar->start();
|
||||
|
||||
$moved = 0;
|
||||
$errors = 0;
|
||||
|
||||
foreach ($mediaRecords as $media) {
|
||||
// Use relative path: {id}/{filename}
|
||||
$relativePath = $media->id . '/' . $media->file_name;
|
||||
|
||||
// Check if source file exists
|
||||
if (!Storage::disk('local')->exists($relativePath)) {
|
||||
$this->newLine();
|
||||
$this->error("Source file not found: {$relativePath}");
|
||||
$errors++;
|
||||
$progressBar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$dryRun) {
|
||||
// Copy file from local to public disk
|
||||
$fileContent = Storage::disk('local')->get($relativePath);
|
||||
Storage::disk('public')->put($relativePath, $fileContent);
|
||||
|
||||
// Verify the file was copied successfully
|
||||
if (Storage::disk('public')->exists($relativePath)) {
|
||||
// Update the database record
|
||||
$media->update([
|
||||
'disk' => 'public',
|
||||
'conversions_disk' => 'public',
|
||||
]);
|
||||
|
||||
// Delete the old file from local disk
|
||||
Storage::disk('local')->delete($relativePath);
|
||||
|
||||
$moved++;
|
||||
} else {
|
||||
throw new \Exception("Failed to copy file to public disk");
|
||||
}
|
||||
} else {
|
||||
$this->newLine();
|
||||
$this->line("Would move: local:{$relativePath} -> public:{$relativePath}");
|
||||
$moved++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->newLine();
|
||||
$this->error("Error moving {$relativePath}: {$e->getMessage()}");
|
||||
$errors++;
|
||||
}
|
||||
|
||||
$progressBar->advance();
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
if ($dryRun) {
|
||||
$this->info("DRY RUN: Would move {$moved} files, {$errors} errors encountered.");
|
||||
$this->info("Run without --dry-run to actually perform the migration.");
|
||||
} else {
|
||||
$this->info("Successfully moved {$moved} files, {$errors} errors encountered.");
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ class EntryResource extends Resource
|
|||
{
|
||||
protected static ?string $model = Entry::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::PencilSquare;
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'title';
|
||||
|
||||
|
|
|
|||
|
|
@ -2,18 +2,16 @@
|
|||
|
||||
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\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
|
||||
|
|
@ -23,16 +21,6 @@ class EntryForm
|
|||
{
|
||||
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)
|
||||
|
|
@ -41,269 +29,18 @@ class EntryForm
|
|||
}),
|
||||
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'))
|
||||
->disk('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')
|
||||
|
|
@ -314,10 +51,11 @@ class EntryForm
|
|||
->label('Select an existing image')
|
||||
->allowHtml()
|
||||
->options(function () {
|
||||
$currentDisk = config('media-library.disk_name', 'public');
|
||||
return Media::where('disk', $currentDisk)
|
||||
return Media::where('model_type', 'temp')
|
||||
->where('model_id', 0)
|
||||
->where('disk', 'public')
|
||||
->latest()
|
||||
->limit(50)
|
||||
->limit(30)
|
||||
->get(['id', 'file_name', 'name', 'disk'])
|
||||
->mapWithKeys(function (Media $item) {
|
||||
try {
|
||||
|
|
@ -330,7 +68,7 @@ class EntryForm
|
|||
"<div class='flex flex-col'>" .
|
||||
"<span class='font-medium text-sm'>{$name}</span>" .
|
||||
"<span class='text-xs text-gray-500'>{$fileName}</span>" .
|
||||
'</div></div>';
|
||||
"</div></div>";
|
||||
|
||||
return [$item->id => $html];
|
||||
} catch (\Exception $e) {
|
||||
|
|
@ -344,22 +82,12 @@ class EntryForm
|
|||
])
|
||||
->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;
|
||||
}
|
||||
|
||||
|
|
@ -368,47 +96,19 @@ class EntryForm
|
|||
}
|
||||
|
||||
$sourceMedia = Media::find($data['image_id']);
|
||||
if (! $sourceMedia) {
|
||||
Log::error('Source media not found', ['image_id' => $data['image_id']]);
|
||||
if (!$sourceMedia || !file_exists($sourceMedia->getPath())) {
|
||||
\Filament\Notifications\Notification::make()
|
||||
->danger()
|
||||
->title('Source image not found in database')
|
||||
->title('Image file not found')
|
||||
->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);
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify record has ID
|
||||
if (!$record->id) {
|
||||
\Filament\Notifications\Notification::make()
|
||||
|
|
@ -422,15 +122,7 @@ class EntryForm
|
|||
$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()
|
||||
]);
|
||||
}
|
||||
->toMediaCollection('featured-image', 'public');
|
||||
|
||||
// Dispatch event for app.js to handle
|
||||
$component->getLivewire()->dispatch('featured-image-added', ['mediaId' => $newMedia->id]);
|
||||
|
|
@ -440,20 +132,12 @@ class EntryForm
|
|||
->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)) {
|
||||
if (file_exists($tempCopy)) {
|
||||
unlink($tempCopy);
|
||||
}
|
||||
}
|
||||
|
|
@ -463,30 +147,8 @@ class EntryForm
|
|||
->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'),
|
||||
|
||||
DatePicker::make('published_at'),
|
||||
RichEditor::make('content')
|
||||
->visible(fn($get) => $get('type') !== 'image')
|
||||
->columnSpanFull()
|
||||
->hintAction(
|
||||
Action::make('picker')
|
||||
|
|
@ -521,7 +183,7 @@ class EntryForm
|
|||
"<div class='flex flex-col'>" .
|
||||
"<span class='font-medium text-sm'>{$name}</span>" .
|
||||
"<span class='text-xs text-gray-500'>{$fileName}</span>" .
|
||||
'</div></div>';
|
||||
"</div></div>";
|
||||
|
||||
return [$url => $html];
|
||||
})->toArray();
|
||||
|
|
|
|||
|
|
@ -17,33 +17,18 @@ class EntriesTable
|
|||
{
|
||||
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(),
|
||||
TextColumn::make('slug')
|
||||
->searchable(),
|
||||
IconColumn::make('is_published')
|
||||
->label('pub')
|
||||
->boolean(),
|
||||
IconColumn::make('is_featured')
|
||||
->label('feat')
|
||||
->boolean(),
|
||||
TextColumn::make('published_at')
|
||||
->date()
|
||||
|
|
|
|||
|
|
@ -22,9 +22,7 @@ class MediaResource extends Resource
|
|||
|
||||
protected static ?string $recordTitleAttribute = 'file_name';
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::Photo
|
||||
|
||||
;
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class ListMedia extends ListRecords
|
|||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
// CreateAction::make(),
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ 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
|
||||
{
|
||||
|
|
@ -26,7 +24,6 @@ class MediaForm
|
|||
Hidden::make('disk')
|
||||
->default('public'),
|
||||
FileUpload::make('file')
|
||||
->multiple() // workaround for Filament v4 single-file bug
|
||||
->label('File')
|
||||
->imageEditor()
|
||||
->imageEditorAspectRatios([
|
||||
|
|
@ -35,23 +32,19 @@ class MediaForm
|
|||
'1:1',
|
||||
])
|
||||
->columnSpanFull()
|
||||
->disk('s3')
|
||||
->disk('public')
|
||||
->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;
|
||||
}
|
||||
|
|
@ -59,32 +52,8 @@ class MediaForm
|
|||
// 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;
|
||||
}
|
||||
}),
|
||||
|
||||
|
||||
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace App\Filament\Resources\Media\Tables;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
|
|
@ -24,8 +23,7 @@ class MediaTable
|
|||
->columns([
|
||||
ImageColumn::make('url')
|
||||
->label('Preview')
|
||||
->getStateUsing(
|
||||
fn($record) =>
|
||||
->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'))
|
||||
|
|
@ -54,7 +52,7 @@ class MediaTable
|
|||
]),
|
||||
])
|
||||
->recordActions([
|
||||
// EditAction::make(),
|
||||
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.
|
||||
|
|
@ -69,13 +67,6 @@ class MediaTable
|
|||
}),
|
||||
])
|
||||
->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) {
|
||||
|
|
|
|||
|
|
@ -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,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);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,38 +5,27 @@ 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 HasRichContent, HasMedia
|
||||
|
||||
class Entry extends Model implements HasMedia, HasRichContent
|
||||
{
|
||||
use HasFactory, HasTags, InteractsWithMedia, InteractsWithRichContent;
|
||||
|
||||
use 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
|
||||
*/
|
||||
|
|
@ -50,19 +39,4 @@ class Entry extends Model implements HasMedia, HasRichContent
|
|||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,17 +6,18 @@ namespace App\Models;
|
|||
|
||||
use Filament\Models\Contracts\FilamentUser;
|
||||
use Filament\Panel;
|
||||
use Illuminate\Container\Attributes\Log;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Facades\Log as FacadesLog;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class User extends Authenticatable implements FilamentUser
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
|
||||
use HasFactory, Notifiable, TwoFactorAuthenticatable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
|
@ -71,11 +72,8 @@ class User extends Authenticatable implements FilamentUser
|
|||
*/
|
||||
public function canAccessPanel(Panel $panel): bool
|
||||
{
|
||||
return $this->email === config('app.admin_email') || $this->role === 'admin';
|
||||
}
|
||||
// FacadesLog::info('Checking admin access for user: ' . $this->email . ' against admin email: ' . config('app.admin_email'));
|
||||
|
||||
public function changes()
|
||||
{
|
||||
return $this->hasMany(Change::class);
|
||||
return $this->email === config('app.admin_email');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
//
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,39 +25,14 @@ 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,
|
||||
])
|
||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
|
||||
->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages')
|
||||
->pages([
|
||||
|
|
@ -68,7 +43,6 @@ class AdminPanelProvider extends PanelProvider
|
|||
AccountWidget::class,
|
||||
FilamentInfoWidget::class,
|
||||
])
|
||||
->navigationItems($navigationItems)
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
|
|
@ -91,144 +65,5 @@ class AdminPanelProvider extends PanelProvider
|
|||
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
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Define the backup directory and the SQLite database file
|
||||
BACKUP_DIR="database/backups"
|
||||
DB_FILE="database/database.sqlite"
|
||||
# DATE="20260124_115644"
|
||||
DATE="20260124_123340"
|
||||
|
||||
# Ensure the DATE variable is set
|
||||
if [ -z "$DATE" ]; then
|
||||
echo "DATE variable is not set. Please set the DATE variable to match the backup file date."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if the backup directory exists
|
||||
if [ ! -d "$BACKUP_DIR" ]; then
|
||||
echo "Backup directory does not exist: $BACKUP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if the SQLite database file exists
|
||||
if [ ! -f "$DB_FILE" ]; then
|
||||
echo "SQLite database file does not exist: $DB_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if there are any matching backup files
|
||||
if ! ls "$BACKUP_DIR"/*"$DATE".sql 1> /dev/null 2>&1; then
|
||||
echo "No backup files found for the date: $DATE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting restore process from backups dated: $DATE"
|
||||
echo "Using database file: $DB_FILE"
|
||||
|
||||
# Loop through each file in the backup directory
|
||||
for file in "$BACKUP_DIR"/*"$DATE".sql; do
|
||||
if [ -f "$file" ]; then
|
||||
echo "Restoring data from $file into $DB_FILE..."
|
||||
|
||||
# Import the SQL file into the SQLite database and log output
|
||||
sqlite3 "$DB_FILE" < "$file" 2>> restore_errors.log
|
||||
|
||||
# Check for errors in the restore process
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Successfully restored $file"
|
||||
else
|
||||
echo "Failed to restore $file. Check restore_errors.log for details."
|
||||
fi
|
||||
|
||||
# Debugging: Print the last 10 lines of the database to verify data
|
||||
# echo "Last 10 rows of the database after restoring $file:"
|
||||
# sqlite3 "$DB_FILE" "SELECT * FROM sqlite_master WHERE type='table';" 2>> restore_errors.log
|
||||
# sqlite3 "$DB_FILE" "SELECT * FROM entries ORDER BY rowid DESC LIMIT 10;" 2>> restore_errors.log
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Restore process completed."
|
||||
|
|
@ -9,16 +9,11 @@
|
|||
"php": "^8.2",
|
||||
"filament/filament": "^4.0",
|
||||
"filament/spatie-laravel-media-library-plugin": "^4.4",
|
||||
"filament/spatie-laravel-tags-plugin": "^4.0",
|
||||
"laravel/fortify": "^1.30",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/reverb": "^1.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"livewire/flux": "^2.9.0",
|
||||
"spatie/laravel-medialibrary": "^11.17",
|
||||
"spatie/laravel-tags": "^4.10"
|
||||
"spatie/laravel-medialibrary": "^11.17"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
|
|
|
|||
1719
composer.lock
generated
1719
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,82 +0,0 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Broadcaster
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default broadcaster that will be used by the
|
||||
| framework when an event needs to be broadcast. You may set this to
|
||||
| any of the connections defined in the "connections" array below.
|
||||
|
|
||||
| Supported: "reverb", "pusher", "ably", "redis", "log", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('BROADCAST_CONNECTION', 'null'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Broadcast Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the broadcast connections that will be used
|
||||
| to broadcast events to other systems or over WebSockets. Samples of
|
||||
| each available type of connection are provided inside this array.
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'reverb' => [
|
||||
'driver' => 'reverb',
|
||||
'key' => env('REVERB_APP_KEY'),
|
||||
'secret' => env('REVERB_APP_SECRET'),
|
||||
'app_id' => env('REVERB_APP_ID'),
|
||||
'options' => [
|
||||
'host' => env('REVERB_API_HOST', '127.0.0.1'),
|
||||
'port' => env('REVERB_API_PORT', 9001),
|
||||
'scheme' => env('REVERB_API_SCHEME', 'http'),
|
||||
'useTLS' => env('REVERB_API_SCHEME', 'http') !== 'https',
|
||||
],
|
||||
'client_options' => [
|
||||
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
|
||||
],
|
||||
],
|
||||
|
||||
'pusher' => [
|
||||
'driver' => 'pusher',
|
||||
'key' => env('PUSHER_APP_KEY'),
|
||||
'secret' => env('PUSHER_APP_SECRET'),
|
||||
'app_id' => env('PUSHER_APP_ID'),
|
||||
'options' => [
|
||||
'cluster' => env('PUSHER_APP_CLUSTER'),
|
||||
'host' => env('PUSHER_HOST') ?: 'api-' . env('PUSHER_APP_CLUSTER', 'mt1') . '.pusher.com',
|
||||
'port' => env('PUSHER_PORT', 443),
|
||||
'scheme' => env('PUSHER_SCHEME', 'https'),
|
||||
'encrypted' => true,
|
||||
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
|
||||
],
|
||||
'client_options' => [
|
||||
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
|
||||
],
|
||||
],
|
||||
|
||||
'ably' => [
|
||||
'driver' => 'ably',
|
||||
'key' => env('ABLY_KEY'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'driver' => 'log',
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'null',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -16,18 +16,18 @@ return [
|
|||
|
||||
'broadcasting' => [
|
||||
|
||||
'echo' => [
|
||||
'broadcaster' => 'reverb',
|
||||
'key' => env('VITE_REVERB_APP_KEY'),
|
||||
'cluster' => env('VITE_REVERB_APP_CLUSTER'),
|
||||
'wsHost' => env('VITE_REVERB_HOST'),
|
||||
'wsPort' => env('VITE_REVERB_PORT'),
|
||||
'wssPort' => env('VITE_REVERB_PORT'),
|
||||
'authEndpoint' => '/broadcasting/auth',
|
||||
'disableStats' => true,
|
||||
'encrypted' => env('VITE_REVERB_SCHEME', 'https') === 'https',
|
||||
'forceTLS' => env('VITE_REVERB_SCHEME', 'https') === 'https',
|
||||
],
|
||||
// 'echo' => [
|
||||
// 'broadcaster' => 'pusher',
|
||||
// 'key' => env('VITE_PUSHER_APP_KEY'),
|
||||
// 'cluster' => env('VITE_PUSHER_APP_CLUSTER'),
|
||||
// 'wsHost' => env('VITE_PUSHER_HOST'),
|
||||
// 'wsPort' => env('VITE_PUSHER_PORT'),
|
||||
// 'wssPort' => env('VITE_PUSHER_PORT'),
|
||||
// 'authEndpoint' => '/broadcasting/auth',
|
||||
// 'disableStats' => true,
|
||||
// 'encrypted' => true,
|
||||
// 'forceTLS' => true,
|
||||
// ],
|
||||
|
||||
],
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ return [
|
|||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'root' => env('AWS_DIRECTORY', ''),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Reverb Server
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default server used by Reverb to handle
|
||||
| incoming messages as well as broadcasting message to all your
|
||||
| connected clients. At this time only "reverb" is supported.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('REVERB_SERVER', 'reverb'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reverb Servers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define details for each of the supported Reverb servers.
|
||||
| Each server has its own configuration options that are defined in
|
||||
| the array below. You should ensure all the options are present.
|
||||
|
|
||||
*/
|
||||
|
||||
'servers' => [
|
||||
|
||||
'reverb' => [
|
||||
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'),
|
||||
'port' => env('REVERB_SERVER_PORT', 8080),
|
||||
'path' => env('REVERB_SERVER_PATH', ''),
|
||||
'hostname' => env('REVERB_HOST'),
|
||||
'options' => [
|
||||
'tls' => [],
|
||||
],
|
||||
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000),
|
||||
'scaling' => [
|
||||
'enabled' => env('REVERB_SCALING_ENABLED', false),
|
||||
'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
|
||||
'server' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
'timeout' => env('REDIS_TIMEOUT', 60),
|
||||
],
|
||||
],
|
||||
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
|
||||
'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reverb Applications
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define how Reverb applications are managed. If you choose
|
||||
| to use the "config" provider, you may define an array of apps which
|
||||
| your server will support, including their connection credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'apps' => [
|
||||
|
||||
'provider' => 'config',
|
||||
|
||||
'apps' => [
|
||||
[
|
||||
'key' => env('REVERB_APP_KEY'),
|
||||
'secret' => env('REVERB_APP_SECRET'),
|
||||
'app_id' => env('REVERB_APP_ID'),
|
||||
'options' => [
|
||||
'host' => env('REVERB_HOST'),
|
||||
'port' => env('REVERB_PORT', 443),
|
||||
'scheme' => env('REVERB_SCHEME', 'https'),
|
||||
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
|
||||
],
|
||||
'allowed_origins' => ['*'],
|
||||
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
|
||||
'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
|
||||
'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'),
|
||||
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000),
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Stateful Domains
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Requests from the following domains / hosts will receive stateful API
|
||||
| authentication cookies. Typically, these should include your local
|
||||
| and production domains which access your API via a frontend SPA.
|
||||
|
|
||||
*/
|
||||
|
||||
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||
'%s%s',
|
||||
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
||||
Sanctum::currentApplicationUrlWithPort(),
|
||||
// Sanctum::currentRequestHost(),
|
||||
))),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sanctum Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This array contains the authentication guards that will be checked when
|
||||
| Sanctum is trying to authenticate a request. If none of these guards
|
||||
| are able to authenticate the request, Sanctum will use the bearer
|
||||
| token that's present on an incoming request for authentication.
|
||||
|
|
||||
*/
|
||||
|
||||
'guard' => ['web'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expiration Minutes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value controls the number of minutes until an issued token will be
|
||||
| considered expired. This will override any values set in the token's
|
||||
| "expires_at" attribute, but first-party sessions are not affected.
|
||||
|
|
||||
*/
|
||||
|
||||
'expiration' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Token Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Sanctum can prefix new tokens in order to take advantage of numerous
|
||||
| security scanning initiatives maintained by open source platforms
|
||||
| that notify developers if they commit tokens into repositories.
|
||||
|
|
||||
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
||||
|
|
||||
*/
|
||||
|
||||
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sanctum Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When authenticating your first-party SPA with Sanctum you may need to
|
||||
| customize some of the middleware Sanctum uses while processing the
|
||||
| request. You may change the middleware listed below as required.
|
||||
|
|
||||
*/
|
||||
|
||||
'middleware' => [
|
||||
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
|
||||
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -26,7 +26,6 @@ return [
|
|||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'directory' => env('AWS_DIRECTORY'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
* The given function generates a URL friendly "slug" from the tag name property before saving it.
|
||||
* Defaults to Str::slug (https://laravel.com/docs/master/helpers#method-str-slug)
|
||||
*/
|
||||
'slugger' => null,
|
||||
|
||||
/*
|
||||
* The fully qualified class name of the tag model.
|
||||
*/
|
||||
'tag_model' => Spatie\Tags\Tag::class,
|
||||
|
||||
/*
|
||||
* The name of the table associated with the taggable morph relation.
|
||||
*/
|
||||
'taggable' => [
|
||||
'table_name' => 'taggables',
|
||||
'morph_name' => 'taggable',
|
||||
|
||||
/*
|
||||
* The fully qualified class name of the pivot model.
|
||||
*/
|
||||
'class_name' => Illuminate\Database\Eloquent\Relations\MorphPivot::class,
|
||||
]
|
||||
];
|
||||
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