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