5 Commits

Author SHA1 Message Date
Tim
101de8916e Add setup for frankenphp symfony docker 2025-05-25 15:18:52 +02:00
Tim
5fcc32de6d Change the snip layout to make it less cluttered 2025-05-24 00:02:23 +02:00
Tim
797d7a2e8f Make snip description optional 2025-05-18 17:10:43 +02:00
Tim
39f6aaea23 Show D in title when in dev env 2025-05-18 16:58:37 +02:00
Tim
116fed5acc Add icon 2025-05-17 16:24:17 +02:00
24 changed files with 575 additions and 91 deletions

34
.dockerignore Normal file
View File

@ -0,0 +1,34 @@
**/*.log
**/*.md
**/*.php~
**/*.dist.php
**/*.dist
**/*.cache
**/._*
**/.dockerignore
**/.DS_Store
**/.git/
**/.gitattributes
**/.gitignore
**/.gitmodules
**/compose.*.yaml
**/compose.*.yml
**/compose.yaml
**/compose.yml
**/docker-compose.*.yaml
**/docker-compose.*.yml
**/docker-compose.yaml
**/docker-compose.yml
**/Dockerfile
**/Thumbs.db
.github/
docs/
public/bundles/
tests/
var/
vendor/
.editorconfig
.env.*.local
.env.local
.env.local.php
.env.test

76
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,76 @@
name: CI
on:
push:
branches:
- main
pull_request: ~
workflow_dispatch: ~
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
tests:
name: Tests
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build Docker images
uses: docker/bake-action@v6
with:
pull: true
load: true
files: |
compose.yaml
compose.override.yaml
set: |
*.cache-from=type=gha,scope=${{github.ref}}
*.cache-from=type=gha,scope=refs/heads/main
*.cache-to=type=gha,scope=${{github.ref}},mode=max
-
name: Start services
run: docker compose up --wait --no-build
-
name: Check HTTP reachability
run: curl -v --fail-with-body http://localhost
-
name: Check HTTPS reachability
if: false # Remove this line when the homepage will be configured, or change the path to check
run: curl -vk --fail-with-body https://localhost
-
name: Check Mercure reachability
run: curl -vkI --fail-with-body https://localhost/.well-known/mercure?topic=test
-
name: Create test database
if: false # Remove this line if Doctrine ORM is installed
run: docker compose exec -T php bin/console -e test doctrine:database:create
-
name: Run migrations
if: false # Remove this line if Doctrine Migrations is installed
run: docker compose exec -T php bin/console -e test doctrine:migrations:migrate --no-interaction
-
name: Run PHPUnit
if: false # Remove this line if PHPUnit is installed
run: docker compose exec -T php bin/phpunit
-
name: Doctrine Schema Validator
if: false # Remove this line if Doctrine ORM is installed
run: docker compose exec -T php bin/console -e test doctrine:schema:validate
lint:
name: Docker Lint
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Lint Dockerfile
uses: hadolint/hadolint-action@v3.1.0

77
Dockerfile Normal file
View File

@ -0,0 +1,77 @@
#syntax=docker/dockerfile:1
# Versions
FROM dunglas/frankenphp:1-php8.4 AS frankenphp_upstream
# The different stages of this Dockerfile are meant to be built into separate images
# https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage
# https://docs.docker.com/compose/compose-file/#target
# Base FrankenPHP image
FROM frankenphp_upstream AS frankenphp_base
WORKDIR /app
VOLUME /app/var/
# persistent / runtime deps
# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y --no-install-recommends \
acl \
file \
gettext \
git \
&& rm -rf /var/lib/apt/lists/*
RUN set -eux; \
install-php-extensions \
@composer \
apcu \
intl \
opcache \
zip \
;
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
ENV COMPOSER_ALLOW_SUPERUSER=1
# Transport to use by Mercure (default to Bolt)
ENV MERCURE_TRANSPORT_URL=bolt:///data/mercure.db
ENV PHP_INI_SCAN_DIR=":$PHP_INI_DIR/app.conf.d"
###> recipes ###
###> doctrine/doctrine-bundle ###
#RUN install-php-extensions pdo pdo_mysql
RUN docker-php-ext-install pdo pdo_mysql
RUN docker-php-ext-enable pdo pdo_mysql
###< doctrine/doctrine-bundle ###
###< recipes ###
COPY --link frankenphp/conf.d/10-app.ini $PHP_INI_DIR/app.conf.d/
COPY --link --chmod=755 frankenphp/docker-entrypoint.sh /usr/local/bin/docker-entrypoint
COPY --link frankenphp/Caddyfile /etc/frankenphp/Caddyfile
ENTRYPOINT ["docker-entrypoint"]
HEALTHCHECK --start-period=60s CMD curl -f http://localhost:2019/metrics || exit 1
CMD [ "frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile" ]
# Dev FrankenPHP image
FROM frankenphp_base AS frankenphp_dev
ENV APP_ENV=dev
ENV XDEBUG_MODE=off
#ENV FRANKENPHP_WORKER_CONFIG=watch
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
RUN set -eux; \
install-php-extensions \
xdebug \
;
COPY --link frankenphp/conf.d/20-app.dev.ini $PHP_INI_DIR/app.conf.d/
CMD [ "frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile", "--watch" ]

32
compose.override.yaml Normal file
View File

@ -0,0 +1,32 @@
# Development environment override
services:
php:
build:
context: .
target: frankenphp_dev
volumes:
- ./:/app
- ./frankenphp/Caddyfile:/etc/frankenphp/Caddyfile:ro
- ./frankenphp/conf.d/20-app.dev.ini:/usr/local/etc/php/app.conf.d/20-app.dev.ini:ro
# If you develop on Mac or Windows you can remove the vendor/ directory
# from the bind-mount for better performance by enabling the next line:
#- /app/vendor
environment:
#FRANKENPHP_WORKER_CONFIG: watch
MERCURE_EXTRA_DIRECTIVES: demo
# See https://xdebug.org/docs/all_settings#mode
XDEBUG_MODE: "${XDEBUG_MODE:-off}"
APP_ENV: "${APP_ENV:-dev}"
extra_hosts:
# Ensure that host.docker.internal is correctly defined on Linux
- host.docker.internal:host-gateway
tty: true
###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ###
###> doctrine/doctrine-bundle ###
database:
ports:
- "3306"
###< doctrine/doctrine-bundle ###

10
compose.prod.yaml Normal file
View File

@ -0,0 +1,10 @@
# Production environment override
services:
php:
build:
context: .
target: frankenphp_prod
environment:
APP_SECRET: ${APP_SECRET}
MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET}
MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET}

69
compose.yaml Normal file
View File

@ -0,0 +1,69 @@
version: "3"
services:
php:
image: ${IMAGES_PREFIX:-}app-php
restart: unless-stopped
depends_on:
- database
environment:
SERVER_NAME: ${SERVER_NAME:-localhost}, php:80
MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
# Run "composer require symfony/orm-pack" to install and configure Doctrine ORM
#DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@database:5432/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-15}&charset=${POSTGRES_CHARSET:-utf8}
DATABASE_URL: mysql://root:password@database:3306/snips?serverVersion=11.7.2-MariaDB
# Run "composer require symfony/mercure-bundle" to install and configure the Mercure integration
MERCURE_URL: ${CADDY_MERCURE_URL:-http://php/.well-known/mercure}
MERCURE_PUBLIC_URL: ${CADDY_MERCURE_PUBLIC_URL:-https://${SERVER_NAME:-localhost}:${HTTPS_PORT:-443}/.well-known/mercure}
MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
# The two next lines can be removed after initial installation
SYMFONY_VERSION: ${SYMFONY_VERSION:-}
STABILITY: ${STABILITY:-stable}
volumes:
- caddy_data:/data
- caddy_config:/config
ports:
# HTTP
- target: 80
published: ${HTTP_PORT:-80}
protocol: tcp
# HTTPS
- target: 443
published: ${HTTPS_PORT:-443}
protocol: tcp
# HTTP/3
- target: 443
published: ${HTTP3_PORT:-443}
protocol: udp
# Mercure is installed as a Caddy module, prevent the Flex recipe from installing another service
###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ###
###> doctrine/doctrine-bundle ###
database:
image: 'mariadb:latest'
environment:
MARIADB_USER: app
MARIADB_PASSWORD: password
MARIADB_DATABASE: snips
MARIADB_ROOT_PASSWORD: password
ports:
# To allow the host machine to access the ports below, modify the lines below.
# For example, to allow the host to connect to port 3306 on the container, you would change
# "3306" to "3306:3306". Where the first port is exposed to the host and the second is the container port.
# See https://docs.docker.com/compose/compose-file/compose-file-v3/#ports for more information.
- '3306'
volumes:
- database_data:/var/lib/mysql
###< doctrine/doctrine-bundle ###
volumes:
caddy_data:
caddy_config:
###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ###
###> doctrine/doctrine-bundle ###
database_data:
###< doctrine/doctrine-bundle ###

View File

@ -82,7 +82,8 @@
"extra": { "extra": {
"symfony": { "symfony": {
"allow-contrib": false, "allow-contrib": false,
"require": "7.2.*" "require": "7.2.*",
"docker": true
} }
} }
} }

2
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "638cbc9841226f386ba27215e24c5410", "content-hash": "4f33fd23b3dd7a367af9ebf6f69f9c0a",
"packages": [ "packages": [
{ {
"name": "dflydev/dot-access-data", "name": "dflydev/dot-access-data",

57
frankenphp/Caddyfile Normal file
View File

@ -0,0 +1,57 @@
{
{$CADDY_GLOBAL_OPTIONS}
frankenphp {
{$FRANKENPHP_CONFIG}
}
}
{$CADDY_EXTRA_CONFIG}
{$SERVER_NAME:localhost} {
log {
{$CADDY_SERVER_LOG_OPTIONS}
# Redact the authorization query parameter that can be set by Mercure
format filter {
request>uri query {
replace authorization REDACTED
}
}
}
root /app/public
encode zstd br gzip
mercure {
# Publisher JWT key
publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
# Subscriber JWT key
subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
# Allow anonymous subscribers (double-check that it's what you want)
anonymous
# Enable the subscription API (double-check that it's what you want)
subscriptions
# Extra directives
{$MERCURE_EXTRA_DIRECTIVES}
}
vulcain
{$CADDY_SERVER_EXTRA_DIRECTIVES}
# Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
header ?Permissions-Policy "browsing-topics=()"
@phpRoute {
not path /.well-known/mercure*
not file {path}
}
rewrite @phpRoute index.php
@frontController path index.php
php @frontController
file_server {
hide *.php
}
}

View File

@ -0,0 +1,13 @@
expose_php = 0
date.timezone = UTC
apc.enable_cli = 1
session.use_strict_mode = 1
zend.detect_unicode = 0
; https://symfony.com/doc/current/performance.html
realpath_cache_size = 4096K
realpath_cache_ttl = 600
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.memory_consumption = 256
opcache.enable_file_override = 1

View File

@ -0,0 +1,5 @@
; See https://docs.docker.com/desktop/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host
; See https://github.com/docker/for-linux/issues/264
; The `client_host` below may optionally be replaced with `discover_client_host=yes`
; Add `start_with_request=yes` to start debug session on each request
xdebug.client_host = host.docker.internal

View File

@ -0,0 +1,5 @@
; https://symfony.com/doc/current/performance.html#use-the-opcache-class-preloading
opcache.preload_user = root
opcache.preload = /app/config/preload.php
; https://symfony.com/doc/current/performance.html#don-t-check-php-files-timestamps
opcache.validate_timestamps = 0

67
frankenphp/docker-entrypoint.sh Executable file
View File

@ -0,0 +1,67 @@
#!/bin/sh
set -e
if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then
# Install the project the first time PHP is started
# After the installation, the following block can be deleted
if [ ! -f composer.json ]; then
rm -Rf tmp/
composer create-project "symfony/skeleton $SYMFONY_VERSION" tmp --stability="$STABILITY" --prefer-dist --no-progress --no-interaction --no-install
cd tmp
cp -Rp . ..
cd -
rm -Rf tmp/
composer require "php:>=$PHP_VERSION" runtime/frankenphp-symfony
composer config --json extra.symfony.docker 'true'
if grep -q ^DATABASE_URL= .env; then
echo 'To finish the installation please press Ctrl+C to stop Docker Compose and run: docker compose up --build --wait'
sleep infinity
fi
fi
if [ -z "$(ls -A 'vendor/' 2>/dev/null)" ]; then
composer install --prefer-dist --no-progress --no-interaction
fi
# Display information about the current project
# Or about an error in project initialization
php bin/console -V
if grep -q ^DATABASE_URL= .env.local; then
echo 'Waiting for database to be ready...'
ATTEMPTS_LEFT_TO_REACH_DATABASE=60
until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do
if [ $? -eq 255 ]; then
# If the Doctrine command exits with 255, an unrecoverable error occurred
ATTEMPTS_LEFT_TO_REACH_DATABASE=0
break
fi
sleep 1
ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1))
echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left."
done
if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then
echo 'The database is not up or not reachable:'
echo "$DATABASE_ERROR"
exit 1
else
echo 'The database is now ready and reachable'
fi
if [ "$( find ./migrations -iname '*.php' -print -quit )" ]; then
echo 'Migrating database...'
php bin/console doctrine:migrations:migrate --no-interaction --all-or-nothing
fi
fi
setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var
setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var
echo 'PHP app ready!'
fi
exec docker-php-entrypoint "$@"

BIN
public/snips.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -167,6 +167,6 @@ class SnipController extends AbstractController
$this->addFlash('success', sprintf('Snip "%s" unarchived', $snip)); $this->addFlash('success', sprintf('Snip "%s" unarchived', $snip));
} }
return $this->redirectToRoute('snip_single', ['snip' => $snip->getId()]); return $this->redirectToRoute('snip_edit', ['snip' => $snip->getId()]);
} }
} }

View File

@ -6,7 +6,6 @@ use App\Entity\Snip;
use App\Service\SnipParser\ParserFactory; use App\Service\SnipParser\ParserFactory;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
@ -36,6 +35,7 @@ class SnipType extends AbstractType
->add('contentName', TextType::class, [ ->add('contentName', TextType::class, [
'label' => 'Change description (optional)', 'label' => 'Change description (optional)',
'mapped' => false, 'mapped' => false,
'required' => false,
]) ])
; ;
} }

View File

@ -14,7 +14,7 @@
] ]
}, },
"doctrine/doctrine-migrations-bundle": { "doctrine/doctrine-migrations-bundle": {
"version": "3.2", "version": "3.4",
"recipe": { "recipe": {
"repo": "github.com/symfony/recipes", "repo": "github.com/symfony/recipes",
"branch": "main", "branch": "main",
@ -39,7 +39,7 @@
] ]
}, },
"symfony/debug-bundle": { "symfony/debug-bundle": {
"version": "6.2", "version": "7.2",
"recipe": { "recipe": {
"repo": "github.com/symfony/recipes", "repo": "github.com/symfony/recipes",
"branch": "main", "branch": "main",
@ -95,7 +95,7 @@
] ]
}, },
"symfony/maker-bundle": { "symfony/maker-bundle": {
"version": "1.48", "version": "1.63",
"recipe": { "recipe": {
"repo": "github.com/symfony/recipes", "repo": "github.com/symfony/recipes",
"branch": "main", "branch": "main",
@ -161,8 +161,7 @@
"branch": "main", "branch": "main",
"version": "7.0", "version": "7.0",
"ref": "0df5844274d871b37fc3816c57a768ffc60a43a5" "ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
}, }
"files": []
}, },
"symfony/validator": { "symfony/validator": {
"version": "7.2", "version": "7.2",
@ -190,6 +189,6 @@
] ]
}, },
"twig/extra-bundle": { "twig/extra-bundle": {
"version": "v3.5.1" "version": "v3.21.0"
} }
} }

View File

@ -3,8 +3,11 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% if title is defined %}{{ title }}{% else %}SNIPS{% endif %}</title> <title>
<link rel="shortcut icon" type="image/jpg" href="/favicon.png"> {% if app.environment == 'dev' %}D{% endif %}
{% if title is defined %}{{ title }}{% else %}SNIPS{% endif %}
</title>
<link rel="shortcut icon" type="image/jpg" href="/snips.png">
{% block css %} {% block css %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
@ -43,9 +46,7 @@
{# javascript block #} {# javascript block #}
{% block js %} {% block js %}
<script src="https://kit.fontawesome.com/3471b6556e.js" crossorigin="anonymous"></script> <script src="https://kit.fontawesome.com/3471b6556e.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js" <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"></script>
integrity="sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js" <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous"></script> crossorigin="anonymous"></script>

View File

@ -1,6 +1,9 @@
<nav class="navbar navbar-expand-md navbar-dark bg-dark" style="z-index: 1;"> <nav class="navbar navbar-expand-md navbar-dark bg-dark" style="z-index: 1;">
<div class="container-fluid"> <div class="container-fluid">
<a title="Snips" class="navbar-brand" href="{{ path('home') }}">SNIPS</a> <a title="Snips" class="navbar-brand" href="{{ path('home') }}">
<img src="/snips.png" width="30" height="30" class="d-inline-block align-top rounded" alt="">
SNIPS
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar" <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar"
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"> aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>

View File

@ -0,0 +1,74 @@
{% extends 'base/one.column.html.twig' %}
{% block body %}
{% if app.user and app.user is same as(snip.createdBy) %}
<a href="{{ path('snip_index') }}" class="btn btn-primary">
<i class="fa fa-arrow-left"></i> Index
</a>
{% else %}
<a href="{{ path('snip_public') }}" class="btn btn-primary">
<i class="fa fa-arrow-left"></i> Index
</a>
{% endif %}
{% block buttons %}{% endblock %}
<br><br>
<div class="card" style="width: 100%;">
<div class="card-header d-flex justify-content-between">
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link {% if active == 'single' %}active{% endif %}"
href="{{ path('snip_single', {'snip': snip.id}) }}">View</a>
</li>
{% if is_granted('edit', snip) %}
<li class="nav-item">
<a class="nav-link {% if active == 'edit' %}active{% endif %}"
href="{{ path('snip_edit', {'snip': snip.id}) }}">Edit</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active == 'versions' %}active{% endif %}"
href="{{ path('version_index', {'snip': snip.id}) }}">Versions</a>
</li>
{% endif %}
</ul>
<span>
<span class="badge bg-secondary">
<i class="fa fa-hashtag"></i> {{ snip.id }}
</span>
{% for tag in snip.tags %}
<span class="badge bg-secondary">{{ tag }}</span>
{% endfor %}
{{ include('user/badge.html.twig', {user: snip.createdBy}) }}
{{ include('snip/badge.html.twig', {snip: snip}) }}
</span>
</div>
<div class="card-body">
{% block cardbody %}{% endblock %}
</div>
<div class="card-footer">
<p class="card-text text-muted">
Current version: {{ snip.activeVersion.id }}
{% if snip.activeVersion == snip.latestVersion %}(latest){% endif %}
Created at {{ include('generic/datetime.badge.html.twig', {datetime: snip.activeVersion.id.dateTime}) }}
</p>
</div>
</div>
{% endblock %}
{% block css %}
{{ parent() }}
<link rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css">
{% endblock %}
{% block js %}
{{ parent() }}
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
<script>
const codeBlocks = document.querySelectorAll('code.hljs');
codeBlocks.forEach((block) => {
hljs.highlightElement(block);
});
</script>
{% endblock %}

View File

@ -1,21 +1,27 @@
{% extends 'base/one.column.html.twig' %} {% extends 'snip/base.html.twig' %}
{% if snip.id %} {% if snip.id %}
{% set title = 'Edit Snip ' ~ snip %} {% set title %}{{ snip }} - Edit{% endset %}
{% else %} {% else %}
{% set title = 'Create Snip' %} {% set title = 'Create Snip' %}
{% endif %} {% endif %}
{% set active = 'edit' %}
{% block body %} {% block buttons %}
{% if snip.id %} {% if is_granted('edit', snip) %}
<a href="{{ path('snip_single', {snip: snip.id}) }}" class="btn btn-primary"> <a href="{{ path('snip_archive', {snip: snip.id}) }}" class="btn btn-secondary">
<i class="fa fa-arrow-left"></i> {% if snip.archived %}
Back <i class="fa fa-undo"></i> Unarchive
{% else %}
<i class="fa fa-archive"></i> Archive
{% endif %}
</a>
<a href="{{ path('snip_delete', {snip: snip.id}) }}" class="btn btn-danger">
<i class="fa fa-trash"></i> Delete
</a> </a>
{% endif %} {% endif %}
<a href="{{ path('snip_index') }}" class="btn btn-info"> {% endblock %}
<i class="fa fa-list"></i>
Index {% block cardbody %}
</a><br><br>
{{ form(form) }} {{ form(form) }}
{% endblock %} {% endblock %}

View File

@ -1,63 +1,16 @@
{% extends 'base/one.column.html.twig' %} {% extends 'snip/base.html.twig' %}
{% set title %}Snip {{ snip }}{% endset %} {% set title %}{{ snip }} - View{% endset %}
{% set active = 'single' %}
{% block body %} {% block buttons %}
{% if app.user %} <a href="{{ path('snip_raw', {snip: snip.id}) }}" class="btn btn-info">
<a href="{{ path('snip_index') }}" class="btn btn-primary">
<i class="fa fa-arrow-left"></i> Back
</a>
{% else %}
<a href="{{ path('snip_public') }}" class="btn btn-primary">
<i class="fa fa-arrow-left"></i> Index
</a>
{% endif %}
{% if is_granted('edit', snip) %}
<a class="btn btn-info" href="{{ path('version_index', {snip: snip.id}) }}">
<i class="fa fa-history" aria-hidden="true"></i> Versions
</a>
<a class="btn btn-warning" href="{{ path('snip_edit', {snip: snip.id}) }}">
<i class="fa fa-pencil" aria-hidden="true"></i> Edit
</a>
<a href="{{ path('snip_archive', {snip: snip.id}) }}" class="btn btn-secondary">
{% if snip.archived %}
<i class="fa fa-undo"></i> Unarchive
{% else %}
<i class="fa fa-archive"></i> Archive
{% endif %}
</a>
<a href="{{ path('snip_delete', {snip: snip.id}) }}" class="btn btn-danger">
<i class="fa fa-trash"></i> Delete
</a>
{% endif %}
<a href="{{ path('snip_raw', {snip: snip.id}) }}" class="btn btn-primary">
<i class="fa fa-eye"></i> Raw <i class="fa fa-eye"></i> Raw
</a> </a>
<br><br> {% endblock %}
<div class="card" style="width: 100%;">
<h4 class="card-header d-flex justify-content-between"> {% block cardbody %}
<span> {{ content|raw }}
{{ snip }} <small class="text-muted">#{{ snip.id }}</small>
</span>
<span>
{% for tag in snip.tags %}
<span class="badge bg-secondary">{{ tag }}</span>
{% endfor %}
{{ include('user/badge.html.twig', {user: snip.createdBy}) }}
{{ include('snip/badge.html.twig', {snip: snip}) }}
</span>
</h4>
<div class="card-body">
{{ content|raw }}
</div>
<div class="card-footer">
<p class="card-text text-muted">
Current version: {{ snip.activeVersion.id }}
{% if snip.activeVersion == snip.latestVersion %}(latest){% endif %}
Created at {{ include('generic/datetime.badge.html.twig', {datetime: snip.activeVersion.id.dateTime}) }}
</p>
</div>
</div>
{% endblock %} {% endblock %}
{% block css %} {% block css %}

View File

@ -1 +1,3 @@
<span class="badge {% if user == app.user %}bg-success{% else %}bg-secondary{% endif %}">{{ user }}</span> <span class="badge {% if user == app.user %}bg-success{% else %}bg-secondary{% endif %}">
<i class="fa fa-user"></i> {{ user }}
</span>

View File

@ -1,18 +1,18 @@
{% extends 'base/one.column.html.twig' %} {% extends 'snip/base.html.twig' %}
{% set title = 'Snip ' ~ snip %} {% set title %}{{ snip }} - Versions{% endset %}
{% set active = 'versions' %}
{% block body %} {% block buttons %}
<a href="{{ path('snip_single', {snip: snip.id}) }}" class="btn btn-primary">
<i class="fa fa-arrow-left"></i> Back
</a>
<a href="{{ path('version_set', {version: snip.latestVersion.id, snip: snip.id}) }}" class="btn btn-warning"> <a href="{{ path('version_set', {version: snip.latestVersion.id, snip: snip.id}) }}" class="btn btn-warning">
<i class="fa fa-refresh"></i> Latest <i class="fa fa-refresh"></i> Latest
</a> </a>
<a href="{{ path('content_compare', {to: snip.activeVersion.id}) }}" class="btn btn-info"> <a href="{{ path('content_compare', {to: snip.activeVersion.id}) }}" class="btn btn-info">
<i class="fa fa-left-right"></i> Compare <i class="fa fa-left-right"></i> Compare
</a> </a>
<br><br> {% endblock %}
{% block cardbody %}
<div class="list-group"> <div class="list-group">
{% for version in snip.snipContents|reverse %} {% for version in snip.snipContents|reverse %}
<a class="list-group-item {% if version.id == snip.activeVersion.id %}list-group-item-success{% endif %} d-flex justify-content-between" <a class="list-group-item {% if version.id == snip.activeVersion.id %}list-group-item-success{% endif %} d-flex justify-content-between"