summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Weipert <git@mail.dweipert.de>2025-08-06 15:21:03 +0200
committerDaniel Weipert <git@mail.dweipert.de>2025-08-06 15:21:03 +0200
commit929c1b90d6b16706f9c63339f5f2a2b42332f3f3 (patch)
treeb2057dc317f6e4476d828f103046c3ab7e834c45
parentd6adc45f0e0380d7f5404549e3774a02b2afaebe (diff)
split into two standalone containers
-rw-r--r--Dockerfile6
-rw-r--r--Justfile9
-rw-r--r--Readme.md14
-rw-r--r--composer.json23
-rw-r--r--docker-compose.dev.yml31
-rw-r--r--docker-compose.yml55
-rw-r--r--print/.gitignore (renamed from .gitignore)3
-rw-r--r--print/Dockerfile14
-rw-r--r--print/composer.json19
-rw-r--r--print/composer.lock (renamed from composer.lock)145
-rw-r--r--print/print.php (renamed from mtg-pdf.php)4
-rw-r--r--web/Dockerfile18
-rw-r--r--web/default.conf.template (renamed from web.conf.template)3
-rw-r--r--web/index.php (renamed from index.php)4
14 files changed, 192 insertions, 156 deletions
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index bbfce84..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,6 +0,0 @@
-FROM php:fpm-alpine
-
-COPY index.php .
-COPY mtg-pdf.php .
-COPY vendor vendor
-COPY AllPrintings.sqlite .
diff --git a/Justfile b/Justfile
index 7e9cae9..f4a6b68 100644
--- a/Justfile
+++ b/Justfile
@@ -1,3 +1,6 @@
-generate deck:
- DECKLIST="{{deck}}" docker compose run cli
- docker compose down
+build:
+ docker build --no-cache -t mtg-print print
+ docker build --no-cache -t mtg-print-web web
+
+get_printings:
+ curl -L https://mtgjson.com/api/v5/AllPrintings.sqlite -o print/AllPrintings.sqlite
diff --git a/Readme.md b/Readme.md
index 6508228..dd726f3 100644
--- a/Readme.md
+++ b/Readme.md
@@ -1 +1,13 @@
-TODO: split into generic package and server service implementation
+## Run web interface
+```
+docker compose up -d
+# add overwrites as needed
+```
+
+## Update printings database
+
+```
+just get_printings
+# or
+curl -L https://mtgjson.com/api/v5/AllPrintings.sqlite -o print/AllPrintings.sqlite
+```
diff --git a/composer.json b/composer.json
deleted file mode 100644
index 1f6c131..0000000
--- a/composer.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "name": "dweipert/mtg-pdf",
- "authors": [
- {
- "name": "Daniel Weipert",
- "email": "code@drogueronin.de"
- }
- ],
- "require": {
- "gotenberg/gotenberg-php": "^1.1",
- "guzzlehttp/guzzle": "^7.8"
- },
- "autoload": {
- "psr-4": {
- "Dweipert\\MtgPdf\\": "src/"
- }
- },
- "config": {
- "allow-plugins": {
- "php-http/discovery": true
- }
- }
-}
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
new file mode 100644
index 0000000..b08d9bf
--- /dev/null
+++ b/docker-compose.dev.yml
@@ -0,0 +1,31 @@
+services:
+ gotenberg:
+ image: "gotenberg/gotenberg:8"
+
+ print:
+ build:
+ context: print
+ depends_on:
+ - "gotenberg"
+ volumes:
+ - "./print:/var/www/html"
+
+ php:
+ image: php:fpm-alpine
+ volumes:
+ - "./web:/var/www/html"
+ - "./print:/var/www/html/print"
+
+ web:
+ build:
+ context: web
+ depends_on:
+ - "gotenberg"
+ - "php"
+ ports:
+ - "8080:80"
+ environment:
+ - "SERVER_NAME=localhost"
+ volumes:
+ - "./web:/var/www/html"
+ - "./print:/var/www/html/print"
diff --git a/docker-compose.yml b/docker-compose.yml
index e8e8280..6f6d28f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,57 +1,20 @@
services:
gotenberg:
- image: "gotenberg/gotenberg:7"
- networks:
- - "internal"
-
- cli:
- image: "php:cli-alpine"
- depends_on:
- - "gotenberg"
- environment:
- - "DECKLIST=${DECKLIST}"
- volumes:
- - "./:/usr/src/app"
- networks:
- - "internal"
- working_dir: "/usr/src/app"
- command: ["php", "-d", "memory_limit=256M", "./mtg-pdf.php"]
- profiles: [cli]
+ image: "gotenberg/gotenberg:8"
php:
- # image: php:fpm-alpine
- build: .
- depends_on:
- - "gotenberg"
- # volumes:
- # - "./:/var/www/html"
- networks:
- - "internal"
+ image: php:fpm-alpine
+ volumes:
+ - "web:/var/www/html"
web:
- image: nginx:alpine
- restart: unless-stopped
+ image: mtg-print-web
depends_on:
+ - "gotenberg"
- "php"
- environment:
- - "DOMAIN=${DOMAIN}"
volumes:
- - "./:/var/www/html"
- - "./web.conf.template:/etc/nginx/templates/web.conf.template"
- networks:
- - "internal"
- - "traefik"
- labels:
- - "traefik.enable=true"
- - "traefik.docker.network=${TRAEFIK_NETWORK}"
- - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${DOMAIN}`)"
- - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.entrypoints=websecure"
- - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.tls=true"
- - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.tls.certresolver=letsencrypt"
+ - "web:/var/www/html"
-networks:
- internal:
- traefik:
- name: "${TRAEFIK_NETWORK}"
- external: true
+volumes:
+ web:
diff --git a/.gitignore b/print/.gitignore
index 1e29f20..9d80500 100644
--- a/.gitignore
+++ b/print/.gitignore
@@ -1,5 +1,2 @@
/vendor/
-/output/
-
AllPrintings.sqlite
-.env
diff --git a/print/Dockerfile b/print/Dockerfile
new file mode 100644
index 0000000..632b917
--- /dev/null
+++ b/print/Dockerfile
@@ -0,0 +1,14 @@
+FROM composer AS composer
+FROM php:fpm-alpine
+
+COPY --from=composer /usr/bin/composer /usr/bin/composer
+
+WORKDIR /var/www/html
+
+COPY composer.* ./
+COPY print.php .
+COPY AllPrintings.sqlite .
+
+RUN composer install
+
+CMD ["php", "./print.php"]
diff --git a/print/composer.json b/print/composer.json
new file mode 100644
index 0000000..b0835ef
--- /dev/null
+++ b/print/composer.json
@@ -0,0 +1,19 @@
+{
+ "name": "mtg-pdf/print",
+ "version": "0.1.0",
+ "authors": [
+ {
+ "name": "Daniel Weipert",
+ "email": "git@mail.dweipert.de"
+ }
+ ],
+ "require": {
+ "gotenberg/gotenberg-php": "^2.14",
+ "guzzlehttp/guzzle": "^7.9"
+ },
+ "config": {
+ "allow-plugins": {
+ "php-http/discovery": true
+ }
+ }
+}
diff --git a/composer.lock b/print/composer.lock
index f3e7063..4e8cc63 100644
--- a/composer.lock
+++ b/print/composer.lock
@@ -4,36 +4,36 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "8866dd682d37b1548722b1ca15cbf6bf",
+ "content-hash": "bf85f87211eb6b1a6fa04723482817f2",
"packages": [
{
"name": "gotenberg/gotenberg-php",
- "version": "v1.1.7",
+ "version": "v2.14.0",
"source": {
"type": "git",
"url": "https://github.com/gotenberg/gotenberg-php.git",
- "reference": "cfe70a9693a9ca58ebdcda6ec6a4ea404219d592"
+ "reference": "748efe0a981b3e8eb676593246a75925448324ad"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/gotenberg/gotenberg-php/zipball/cfe70a9693a9ca58ebdcda6ec6a4ea404219d592",
- "reference": "cfe70a9693a9ca58ebdcda6ec6a4ea404219d592",
+ "url": "https://api.github.com/repos/gotenberg/gotenberg-php/zipball/748efe0a981b3e8eb676593246a75925448324ad",
+ "reference": "748efe0a981b3e8eb676593246a75925448324ad",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"guzzlehttp/psr7": "^1 || ^2.1",
- "php": "^7.4|^8.0|^8.1|^8.2|^8.3",
+ "php": "^8.1|^8.2|^8.3|^8.4",
"php-http/discovery": "^1.14",
"psr/http-client": "^1.0",
"psr/http-message": "^1.0|^2.0"
},
"require-dev": {
- "doctrine/coding-standard": "^9.0",
- "pestphp/pest": "^1.21",
- "phpstan/phpstan": "^1.2",
- "squizlabs/php_codesniffer": "^3.6"
+ "doctrine/coding-standard": "^12.0",
+ "pestphp/pest": "^2.28",
+ "phpstan/phpstan": "^1.12",
+ "squizlabs/php_codesniffer": "^3.10"
},
"type": "library",
"autoload": {
@@ -77,28 +77,34 @@
],
"support": {
"issues": "https://github.com/gotenberg/gotenberg-php/issues",
- "source": "https://github.com/gotenberg/gotenberg-php/tree/v1.1.7"
+ "source": "https://github.com/gotenberg/gotenberg-php/tree/v2.14.0"
},
- "time": "2023-11-22T19:42:36+00:00"
+ "funding": [
+ {
+ "url": "https://github.com/gulien",
+ "type": "github"
+ }
+ ],
+ "time": "2025-05-20T10:00:34+00:00"
},
{
"name": "guzzlehttp/guzzle",
- "version": "7.8.0",
+ "version": "7.9.3",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
- "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9"
+ "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/1110f66a6530a40fe7aea0378fe608ee2b2248f9",
- "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77",
+ "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77",
"shasum": ""
},
"require": {
"ext-json": "*",
- "guzzlehttp/promises": "^1.5.3 || ^2.0.1",
- "guzzlehttp/psr7": "^1.9.1 || ^2.5.1",
+ "guzzlehttp/promises": "^1.5.3 || ^2.0.3",
+ "guzzlehttp/psr7": "^2.7.0",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
@@ -107,11 +113,11 @@
"psr/http-client-implementation": "1.0"
},
"require-dev": {
- "bamarni/composer-bin-plugin": "^1.8.1",
+ "bamarni/composer-bin-plugin": "^1.8.2",
"ext-curl": "*",
- "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999",
+ "guzzle/client-integration-tests": "3.0.2",
"php-http/message-factory": "^1.1",
- "phpunit/phpunit": "^8.5.29 || ^9.5.23",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20",
"psr/log": "^1.1 || ^2.0 || ^3.0"
},
"suggest": {
@@ -189,7 +195,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
- "source": "https://github.com/guzzle/guzzle/tree/7.8.0"
+ "source": "https://github.com/guzzle/guzzle/tree/7.9.3"
},
"funding": [
{
@@ -205,28 +211,28 @@
"type": "tidelift"
}
],
- "time": "2023-08-27T10:20:53+00:00"
+ "time": "2025-03-27T13:37:11+00:00"
},
{
"name": "guzzlehttp/promises",
- "version": "2.0.1",
+ "version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
- "reference": "111166291a0f8130081195ac4556a5587d7f1b5d"
+ "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d",
- "reference": "111166291a0f8130081195ac4556a5587d7f1b5d",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c",
+ "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
- "bamarni/composer-bin-plugin": "^1.8.1",
- "phpunit/phpunit": "^8.5.29 || ^9.5.23"
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20"
},
"type": "library",
"extra": {
@@ -272,7 +278,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
- "source": "https://github.com/guzzle/promises/tree/2.0.1"
+ "source": "https://github.com/guzzle/promises/tree/2.2.0"
},
"funding": [
{
@@ -288,20 +294,20 @@
"type": "tidelift"
}
],
- "time": "2023-08-03T15:11:55+00:00"
+ "time": "2025-03-27T13:27:01+00:00"
},
{
"name": "guzzlehttp/psr7",
- "version": "2.6.1",
+ "version": "2.7.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
- "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727"
+ "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/psr7/zipball/be45764272e8873c72dbe3d2edcfdfcc3bc9f727",
- "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16",
+ "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16",
"shasum": ""
},
"require": {
@@ -315,9 +321,9 @@
"psr/http-message-implementation": "1.0"
},
"require-dev": {
- "bamarni/composer-bin-plugin": "^1.8.1",
- "http-interop/http-factory-tests": "^0.9",
- "phpunit/phpunit": "^8.5.29 || ^9.5.23"
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "http-interop/http-factory-tests": "0.9.0",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
@@ -388,7 +394,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
- "source": "https://github.com/guzzle/psr7/tree/2.6.1"
+ "source": "https://github.com/guzzle/psr7/tree/2.7.1"
},
"funding": [
{
@@ -404,20 +410,20 @@
"type": "tidelift"
}
],
- "time": "2023-08-27T10:13:57+00:00"
+ "time": "2025-03-27T12:30:47+00:00"
},
{
"name": "php-http/discovery",
- "version": "1.19.1",
+ "version": "1.20.0",
"source": {
"type": "git",
"url": "https://github.com/php-http/discovery.git",
- "reference": "57f3de01d32085fea20865f9b16fb0e69347c39e"
+ "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-http/discovery/zipball/57f3de01d32085fea20865f9b16fb0e69347c39e",
- "reference": "57f3de01d32085fea20865f9b16fb0e69347c39e",
+ "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d",
+ "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d",
"shasum": ""
},
"require": {
@@ -441,7 +447,8 @@
"php-http/httplug": "^1.0 || ^2.0",
"php-http/message-factory": "^1.0",
"phpspec/phpspec": "^5.1 || ^6.1 || ^7.3",
- "symfony/phpunit-bridge": "^6.2"
+ "sebastian/comparator": "^3.0.5 || ^4.0.8",
+ "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1"
},
"type": "composer-plugin",
"extra": {
@@ -480,9 +487,9 @@
],
"support": {
"issues": "https://github.com/php-http/discovery/issues",
- "source": "https://github.com/php-http/discovery/tree/1.19.1"
+ "source": "https://github.com/php-http/discovery/tree/1.20.0"
},
- "time": "2023-07-11T07:02:26+00:00"
+ "time": "2024-10-02T11:20:13+00:00"
},
{
"name": "psr/http-client",
@@ -538,20 +545,20 @@
},
{
"name": "psr/http-factory",
- "version": "1.0.2",
+ "version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
- "reference": "e616d01114759c4c489f93b099585439f795fe35"
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35",
- "reference": "e616d01114759c4c489f93b099585439f795fe35",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
"shasum": ""
},
"require": {
- "php": ">=7.0.0",
+ "php": ">=7.1",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
@@ -575,7 +582,7 @@
"homepage": "https://www.php-fig.org/"
}
],
- "description": "Common interfaces for PSR-7 HTTP message factories",
+ "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
"keywords": [
"factory",
"http",
@@ -587,9 +594,9 @@
"response"
],
"support": {
- "source": "https://github.com/php-fig/http-factory/tree/1.0.2"
+ "source": "https://github.com/php-fig/http-factory"
},
- "time": "2023-04-10T20:10:41+00:00"
+ "time": "2024-04-15T12:06:14+00:00"
},
{
"name": "psr/http-message",
@@ -690,16 +697,16 @@
},
{
"name": "symfony/deprecation-contracts",
- "version": "v3.4.0",
+ "version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
- "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf"
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf",
- "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
"shasum": ""
},
"require": {
@@ -707,12 +714,12 @@
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-main": "3.4-dev"
- },
"thanks": {
- "name": "symfony/contracts",
- "url": "https://github.com/symfony/contracts"
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
}
},
"autoload": {
@@ -737,7 +744,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0"
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
},
"funding": [
{
@@ -753,16 +760,16 @@
"type": "tidelift"
}
],
- "time": "2023-05-23T14:45:45+00:00"
+ "time": "2024-09-25T14:21:43+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
- "stability-flags": [],
+ "stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
- "platform": [],
- "platform-dev": [],
+ "platform": {},
+ "platform-dev": {},
"plugin-api-version": "2.6.0"
}
diff --git a/mtg-pdf.php b/print/print.php
index 55b8112..0e1137f 100644
--- a/mtg-pdf.php
+++ b/print/print.php
@@ -33,7 +33,7 @@ if (empty($cards)) {
* Select data from DB
*/
-$db = new \PDO('sqlite:AllPrintings.sqlite');
+$db = new \PDO('sqlite:' . __DIR__ . '/AllPrintings.sqlite');
$images = [];
foreach ($cards as $card) {
@@ -129,7 +129,7 @@ $html = str_replace('{{imgs}}', implode('', $imgs), $template);
* Build PDF
*/
-$request = Gotenberg::chromium('gotenberg:3000')
+$request = Gotenberg::chromium('gotenberg:3000')->pdf()
->paperSize(8.27, 11.7) # A4
->margins(0.3, 0, 0.3, 0)
->outputFilename(date("Ymd_His"))
diff --git a/web/Dockerfile b/web/Dockerfile
new file mode 100644
index 0000000..d80a89e
--- /dev/null
+++ b/web/Dockerfile
@@ -0,0 +1,18 @@
+FROM mtg-print AS mtg-print
+FROM nginx:alpine
+
+RUN \
+ mkdir -p /var/www/html/print && \
+ mkdir -p /var/www/html/print/vendor && \
+ chmod 557 /var/www/html/print
+
+COPY --from=mtg-print \
+ /var/www/html/AllPrintings.sqlite /var/www/html/print.php \
+ /var/www/html/print/
+
+COPY --from=mtg-print \
+ /var/www/html/vendor/ \
+ /var/www/html/print/vendor/
+
+COPY default.conf.template /etc/nginx/templates/
+COPY index.php /var/www/html/
diff --git a/web.conf.template b/web/default.conf.template
index 36016fa..888aa04 100644
--- a/web.conf.template
+++ b/web/default.conf.template
@@ -1,5 +1,6 @@
server {
- server_name ${DOMAIN};
+ server_name localhost;
+ listen 80;
root /var/www/html;
index index.php;
diff --git a/index.php b/web/index.php
index 79920f7..2aec31f 100644
--- a/index.php
+++ b/web/index.php
@@ -2,7 +2,7 @@
if (isset($_POST['decklist'])) {
$_ENV['DECKLIST'] = $_POST['decklist'];
- include __DIR__ . '/mtg-pdf.php';
+ include __DIR__ . '/print/print.php';
exit;
}
@@ -29,7 +29,7 @@ PLCHLDR;
</head>
<body>
<form action="" method="POST">
- <textarea name="decklist" placeholder="<?php echo $placeholder; ?>" rows="10" cols="40" style="max-width: 100%;"></textarea>
+ <textarea name="decklist" placeholder="<?php echo $placeholder; ?>" rows="10" cols="40" style="max-width: 100%;" required></textarea>
<button>Submit</button>
</form>
<hr>