diff --git a/.gitignore b/.gitignore index 48b8bf9..e69de29 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +0,0 @@ -vendor/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..78318b8 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +# Run the application +run: + go run ./cmd + +# Build the application binary into a bin/ folder +build: + go build -o bin/website ./cmd + @echo "Project built in bin directory" + +# Clean up binaries and build artifacts +clean: + rm -rf bin/ + @echo "Build artifacts removed." diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..266f808 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "log" + "net/http" + + "jh/website/internal/handlers" +) + +func main() { + mux := http.NewServeMux() + + // Resolve static dir relative to the binary's location + mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + + // Routes + mux.HandleFunc("GET /", handlers.Home) + + log.Println("Server starting on http://localhost:8080") + if err := http.ListenAndServe(":8080", mux); err != nil { + log.Fatal(err) + } +} diff --git a/composer.json b/composer.json deleted file mode 100644 index 75720b6..0000000 --- a/composer.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "yourname/portfolio", - "description": "Personal developer portfolio", - "type": "project", - "require": { - "php": ">=8.1", - "league/route": "^5.1", - "nyholm/psr7": "^1.8", - "laminas/laminas-httphandlerrunner": "^2.9", - "nyholm/psr7-server": "^1.1" - }, - "autoload": { - "psr-4": { - "App\\": "src/" - } - } -} diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 9c31430..0000000 --- a/composer.lock +++ /dev/null @@ -1,761 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "323c40f7ef7025d5fdc123b6e18ec5df", - "packages": [ - { - "name": "laminas/laminas-httphandlerrunner", - "version": "2.13.0", - "source": { - "type": "git", - "url": "https://github.com/laminas/laminas-httphandlerrunner.git", - "reference": "181eaeeb838ad3d80fbbcfb0657a46bc212bbd4e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-httphandlerrunner/zipball/181eaeeb838ad3d80fbbcfb0657a46bc212bbd4e", - "reference": "181eaeeb838ad3d80fbbcfb0657a46bc212bbd4e", - "shasum": "" - }, - "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", - "psr/http-message": "^1.0 || ^2.0", - "psr/http-message-implementation": "^1.0 || ^2.0", - "psr/http-server-handler": "^1.0" - }, - "require-dev": { - "laminas/laminas-coding-standard": "~3.1.0", - "laminas/laminas-diactoros": "^3.6.0", - "phpunit/phpunit": "^10.5.46", - "psalm/plugin-phpunit": "^0.19.5", - "vimeo/psalm": "^6.10.3" - }, - "type": "library", - "extra": { - "laminas": { - "config-provider": "Laminas\\HttpHandlerRunner\\ConfigProvider" - } - }, - "autoload": { - "psr-4": { - "Laminas\\HttpHandlerRunner\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "Execute PSR-15 RequestHandlerInterface instances and emit responses they generate.", - "homepage": "https://laminas.dev", - "keywords": [ - "components", - "laminas", - "mezzio", - "psr-15", - "psr-7" - ], - "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-httphandlerrunner/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-httphandlerrunner/issues", - "rss": "https://github.com/laminas/laminas-httphandlerrunner/releases.atom", - "source": "https://github.com/laminas/laminas-httphandlerrunner" - }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "time": "2025-10-12T20:58:29+00:00" - }, - { - "name": "league/route", - "version": "5.1.2", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/route.git", - "reference": "adf9b961dc5ffdbcffb2b8d7963c7978f2794c92" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/route/zipball/adf9b961dc5ffdbcffb2b8d7963c7978f2794c92", - "reference": "adf9b961dc5ffdbcffb2b8d7963c7978f2794c92", - "shasum": "" - }, - "require": { - "nikic/fast-route": "^1.3", - "opis/closure": "^3.5.5", - "php": "^7.2 || ^8.0", - "psr/container": "^1.0|^2.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.0.1", - "psr/http-server-handler": "^1.0.1", - "psr/http-server-middleware": "^1.0.1", - "psr/simple-cache": "^1.0" - }, - "replace": { - "orno/http": "~1.0", - "orno/route": "~1.0" - }, - "require-dev": { - "laminas/laminas-diactoros": "^2.3", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpunit/phpunit": "^8.5", - "roave/security-advisories": "dev-master", - "scrutinizer/ocular": "^1.8", - "squizlabs/php_codesniffer": "^3.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev", - "dev-2.x": "2.x-dev", - "dev-3.x": "3.x-dev", - "dev-4.x": "4.x-dev", - "dev-5.x": "5.x-dev", - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Route\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Phil Bennett", - "email": "philipobenito@gmail.com", - "role": "Developer" - } - ], - "description": "Fast routing and dispatch component including PSR-15 middleware, built on top of FastRoute.", - "homepage": "https://github.com/thephpleague/route", - "keywords": [ - "dispatcher", - "league", - "psr-15", - "psr-7", - "psr15", - "psr7", - "route", - "router" - ], - "support": { - "issues": "https://github.com/thephpleague/route/issues", - "source": "https://github.com/thephpleague/route/tree/5.1.2" - }, - "funding": [ - { - "url": "https://github.com/philipobenito", - "type": "github" - } - ], - "time": "2021-07-30T08:33:09+00:00" - }, - { - "name": "nikic/fast-route", - "version": "v1.3.0", - "source": { - "type": "git", - "url": "https://github.com/nikic/FastRoute.git", - "reference": "181d480e08d9476e61381e04a71b34dc0432e812" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", - "reference": "181d480e08d9476e61381e04a71b34dc0432e812", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35|~5.7" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "FastRoute\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov", - "email": "nikic@php.net" - } - ], - "description": "Fast request router for PHP", - "keywords": [ - "router", - "routing" - ], - "support": { - "issues": "https://github.com/nikic/FastRoute/issues", - "source": "https://github.com/nikic/FastRoute/tree/master" - }, - "time": "2018-02-13T20:26:39+00:00" - }, - { - "name": "nyholm/psr7", - "version": "1.8.2", - "source": { - "type": "git", - "url": "https://github.com/Nyholm/psr7.git", - "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", - "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", - "shasum": "" - }, - "require": { - "php": ">=7.2", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.1 || ^2.0" - }, - "provide": { - "php-http/message-factory-implementation": "1.0", - "psr/http-factory-implementation": "1.0", - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "http-interop/http-factory-tests": "^0.9", - "php-http/message-factory": "^1.0", - "php-http/psr7-integration-tests": "^1.0", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", - "symfony/error-handler": "^4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Nyholm\\Psr7\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - }, - { - "name": "Martijn van der Ven", - "email": "martijn@vanderven.se" - } - ], - "description": "A fast PHP7 implementation of PSR-7", - "homepage": "https://tnyholm.se", - "keywords": [ - "psr-17", - "psr-7" - ], - "support": { - "issues": "https://github.com/Nyholm/psr7/issues", - "source": "https://github.com/Nyholm/psr7/tree/1.8.2" - }, - "funding": [ - { - "url": "https://github.com/Zegnat", - "type": "github" - }, - { - "url": "https://github.com/nyholm", - "type": "github" - } - ], - "time": "2024-09-09T07:06:30+00:00" - }, - { - "name": "nyholm/psr7-server", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/Nyholm/psr7-server.git", - "reference": "4335801d851f554ca43fa6e7d2602141538854dc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/4335801d851f554ca43fa6e7d2602141538854dc", - "reference": "4335801d851f554ca43fa6e7d2602141538854dc", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.0 || ^2.0" - }, - "require-dev": { - "nyholm/nsa": "^1.1", - "nyholm/psr7": "^1.3", - "phpunit/phpunit": "^7.0 || ^8.5 || ^9.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Nyholm\\Psr7Server\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - }, - { - "name": "Martijn van der Ven", - "email": "martijn@vanderven.se" - } - ], - "description": "Helper classes to handle PSR-7 server requests", - "homepage": "http://tnyholm.se", - "keywords": [ - "psr-17", - "psr-7" - ], - "support": { - "issues": "https://github.com/Nyholm/psr7-server/issues", - "source": "https://github.com/Nyholm/psr7-server/tree/1.1.0" - }, - "funding": [ - { - "url": "https://github.com/Zegnat", - "type": "github" - }, - { - "url": "https://github.com/nyholm", - "type": "github" - } - ], - "time": "2023-11-08T09:30:43+00:00" - }, - { - "name": "opis/closure", - "version": "3.7.0", - "source": { - "type": "git", - "url": "https://github.com/opis/closure.git", - "reference": "b1a22a6be71c1263f3ca6e68f00b3fd4d394abc4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/opis/closure/zipball/b1a22a6be71c1263f3ca6e68f00b3fd4d394abc4", - "reference": "b1a22a6be71c1263f3ca6e68f00b3fd4d394abc4", - "shasum": "" - }, - "require": { - "php": "^5.4 || ^7.0 || ^8.0" - }, - "require-dev": { - "jeremeamia/superclosure": "^2.0", - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.6.x-dev" - } - }, - "autoload": { - "files": [ - "functions.php" - ], - "psr-4": { - "Opis\\Closure\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marius Sarca", - "email": "marius.sarca@gmail.com" - }, - { - "name": "Sorin Sarca", - "email": "sarca_sorin@hotmail.com" - } - ], - "description": "A library that can be used to serialize closures (anonymous functions) and arbitrary objects.", - "homepage": "https://opis.io/closure", - "keywords": [ - "anonymous functions", - "closure", - "function", - "serializable", - "serialization", - "serialize" - ], - "support": { - "issues": "https://github.com/opis/closure/issues", - "source": "https://github.com/opis/closure/tree/3.7.0" - }, - "time": "2025-07-08T20:30:08+00:00" - }, - { - "name": "psr/container", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" - }, - "time": "2021-11-05T16:47:00+00:00" - }, - { - "name": "psr/http-factory", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "shasum": "" - }, - "require": { - "php": ">=7.1", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", - "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-factory" - }, - "time": "2024-04-15T12:06:14+00:00" - }, - { - "name": "psr/http-message", - "version": "1.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", - "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-message/tree/1.1" - }, - "time": "2023-04-04T09:50:52+00:00" - }, - { - "name": "psr/http-server-handler", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-server-handler.git", - "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", - "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", - "shasum": "" - }, - "require": { - "php": ">=7.0", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Server\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP server-side request handler", - "keywords": [ - "handler", - "http", - "http-interop", - "psr", - "psr-15", - "psr-7", - "request", - "response", - "server" - ], - "support": { - "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" - }, - "time": "2023-04-10T20:06:20+00:00" - }, - { - "name": "psr/http-server-middleware", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-server-middleware.git", - "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", - "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", - "shasum": "" - }, - "require": { - "php": ">=7.0", - "psr/http-message": "^1.0 || ^2.0", - "psr/http-server-handler": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Server\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP server-side middleware", - "keywords": [ - "http", - "http-interop", - "middleware", - "psr", - "psr-15", - "psr-7", - "request", - "response" - ], - "support": { - "issues": "https://github.com/php-fig/http-server-middleware/issues", - "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" - }, - "time": "2023-04-11T06:14:47+00:00" - }, - { - "name": "psr/simple-cache", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/simple-cache.git", - "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", - "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\SimpleCache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interfaces for simple caching", - "keywords": [ - "cache", - "caching", - "psr", - "psr-16", - "simple-cache" - ], - "support": { - "source": "https://github.com/php-fig/simple-cache/tree/master" - }, - "time": "2017-10-23T01:57:42+00:00" - } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": {}, - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "php": ">=8.1" - }, - "platform-dev": {}, - "plugin-api-version": "2.6.0" -} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c5f9fe5 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module jh/website + +go 1.26.1 diff --git a/internal/handlers/home.go b/internal/handlers/home.go new file mode 100644 index 0000000..8729135 --- /dev/null +++ b/internal/handlers/home.go @@ -0,0 +1,12 @@ +package handlers + +import "net/http" + +type HomeData struct { + Title string +} + +func Home(w http.ResponseWriter, r *http.Request) { + data := HomeData{Title: "Home"} + view(w, r, "home", data) +} diff --git a/internal/handlers/utils.go b/internal/handlers/utils.go new file mode 100644 index 0000000..7dac199 --- /dev/null +++ b/internal/handlers/utils.go @@ -0,0 +1,32 @@ +package handlers + +import ( + "html/template" + "net/http" + "path/filepath" +) + +// Renders a full page by combining the base template with a page template +// Parsed together so the page can define blocks needed for base template +func render(w http.ResponseWriter, _ *http.Request, page string, data any) { + files := []string{ + filepath.Join("internal", "templates", "layouts", "base.html"), + filepath.Join("internal", "templates", "pages", page+".html"), + } + + tmpl, err := template.ParseFiles(files...) + if err != nil { + http.Error(w, "template error: " + err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html") + + if err := tmpl.ExecuteTemplate(w, "base", data); err != nil { + http.Error(w, "render error: " + err.Error(), http.StatusInternalServerError) + } +} + +func view(w http.ResponseWriter, r *http.Request, page string, data any) { + render(w, r, page, data) +} diff --git a/internal/templates/layouts/base.html b/internal/templates/layouts/base.html new file mode 100644 index 0000000..babe73b --- /dev/null +++ b/internal/templates/layouts/base.html @@ -0,0 +1,33 @@ +{{define "base"}} + + + + + + {{.Title}} + + + + + + +
+ +
+ +
+ {{template "content" .}} +
+ + + + + + +{{end}} diff --git a/internal/templates/pages/home.html b/internal/templates/pages/home.html new file mode 100644 index 0000000..a599989 --- /dev/null +++ b/internal/templates/pages/home.html @@ -0,0 +1,7 @@ +{{define "content"}} +
+

Hi, I'm Jason

+

PHP & Go developer. I build web applications and tools.

+ Codeberg +
+{{end}} diff --git a/public/index.php b/public/index.php deleted file mode 100644 index b7c5de2..0000000 --- a/public/index.php +++ /dev/null @@ -1,36 +0,0 @@ -fromGlobals(); - - // --- Router setup --- - $strategy = new ApplicationStrategy(); - $router = new Router(); - - // --- Routes --- - $router->get('/', [HomeController::class, 'index']); - - // --- Dispatch and emit --- - try { - $response = $router->dispatch($request); - } catch (\League\Route\Http\Exception\NotFoundException $e) { - $response = (new Psr17Factory())->createResponse(404); - $response->getBody()->write('

404 — Page not found

'); - } catch (\Throwable $e) { - $response = (new Psr17Factory())->createResponse(500); - $response->getBody()->write('

500 — Server error

'); - } - - (new SapiEmitter())->emit($response); - diff --git a/src/Controllers/BaseController.php b/src/Controllers/BaseController.php deleted file mode 100644 index 2651717..0000000 --- a/src/Controllers/BaseController.php +++ /dev/null @@ -1,73 +0,0 @@ -factory = new Psr17Factory(); - } - - /** - * Render a template — the template itself is responsible for including any layout. - */ - protected function render(string $template, array $data = [], int $status = 200): ResponseInterface - { - ob_start(); - extract($data); - require __DIR__ . '/../views/' . $template . '.php'; - $html = ob_get_clean(); - - return $this->response($html, $status); - } - - /** - * Automatically return a partial for HTMX requests, full page otherwise. - * Each template is responsible for including its own layout if needed. - */ - protected function view( - ServerRequestInterface $request, - string $fullTemplate, - string $partialTemplate, - array $data = [] - ): ResponseInterface { - $template = $this->isHtmx($request) ? $partialTemplate : $fullTemplate; - - return $this->render($template, $data); - } - - /** - * Return a JSON response. - */ - protected function json(array $data, int $status = 200): ResponseInterface - { - return $this->response(json_encode($data), $status, 'application/json'); - } - - /** - * Check if the request came from HTMX. - */ - protected function isHtmx(ServerRequestInterface $request): bool - { - return $request->hasHeader('HX-Request'); - } - - /** - * Build a basic HTML response. - */ - private function response(string $body, int $status = 200, string $contentType = 'text/html'): ResponseInterface - { - $response = $this->factory->createResponse($status); - $response->getBody()->write($body); - return $response->withHeader('Content-Type', $contentType); - } -} diff --git a/src/Controllers/HomeController.php b/src/Controllers/HomeController.php deleted file mode 100644 index 43e3e39..0000000 --- a/src/Controllers/HomeController.php +++ /dev/null @@ -1,16 +0,0 @@ -render('pages/home'); - } -} diff --git a/src/views/pages/home.php b/src/views/pages/home.php deleted file mode 100644 index 1f230c8..0000000 --- a/src/views/pages/home.php +++ /dev/null @@ -1,2 +0,0 @@ -

Jason testing, Here!

- diff --git a/static/css/app.css b/static/css/app.css new file mode 100644 index 0000000..136769c --- /dev/null +++ b/static/css/app.css @@ -0,0 +1,4 @@ +body { + background-color: #191919; + color: #ffffff; +} diff --git a/static/css/index.css b/static/css/index.css new file mode 100644 index 0000000..8f4f0ce --- /dev/null +++ b/static/css/index.css @@ -0,0 +1,467 @@ +/** + * By Oskar Wickström + * Licensed under the MIT License (https://github.com/owickstrom/the-monospace-web/blob/main/LICENSE.md) + **/ +@import url('https://fonts.cdnfonts.com/css/jetbrains-mono-2'); + +:root { + --font-family: "JetBrains Mono", monospace; + --line-height: 1.20rem; + --border-thickness: 2px; + --text-color: #000; + --text-color-alt: #666; + --background-color: #fff; + --background-color-alt: #eee; + + --font-weight-normal: 500; + --font-weight-medium: 600; + --font-weight-bold: 800; + + font-family: var(--font-family); + font-optical-sizing: auto; + font-weight: var(--font-weight-normal); + font-style: normal; + font-variant-numeric: tabular-nums lining-nums; + font-size: 18px; +} + +@media (prefers-color-scheme: dark) { + :root { + --text-color: #fff; + --text-color-alt: #aaa; + --background-color: #000; + --background-color-alt: #111; + } +} + +* { + box-sizing: border-box; +} + + +* + * { + margin-top: var(--line-height); +} + +html { + display: flex; + width: 100%; + margin: 0; + padding: 0; + flex-direction: column; + align-items: center; + background: var(--background-color); + color: var(--text-color); +} + +body { + position: relative; + width: 100%; + margin: 0; + padding: var(--line-height) 2ch; + max-width: calc(min(80ch, round(down, 100%, 1ch))); + line-height: var(--line-height); + overflow-x: hidden; +} + +@media screen and (max-width: 480px) { + :root { + font-size: 14px; + } + body { + padding: var(--line-height) 1ch; + } +} + +h1, h2, h3, h4, h5, h6 { + font-weight: var(--font-weight-bold); + margin: calc(var(--line-height) * 2) 0 var(--line-height); + line-height: var(--line-height); +} + +h1 { + font-size: 2rem; + line-height: calc(2 * var(--line-height)); + margin-bottom: calc(var(--line-height) * 2); + text-transform: uppercase; +} +h2 { + font-size: 1rem; + text-transform: uppercase; +} + +hr { + position: relative; + display: block; + height: var(--line-height); + margin: calc(var(--line-height) * 1.5) 0; + border: none; + color: var(--text-color); +} +hr:after { + display: block; + content: ""; + position: absolute; + top: calc(var(--line-height) / 2 - var(--border-thickness)); + left: 0; + width: 100%; + border-top: calc(var(--border-thickness) * 3) double var(--text-color); + height: 0; +} + +a { + text-decoration-thickness: var(--border-thickness); +} + +a:link, a:visited { + color: var(--text-color); +} + +p { + margin-bottom: var(--line-height); +} + +strong { + font-weight: var(--font-weight-bold); +} +em { + font-style: italic; +} + +sub { + position: relative; + display: inline-block; + margin: 0; + vertical-align: sub; + line-height: 0; + width: calc(1ch / 0.75); + font-size: .75rem; +} + +table { + position: relative; + top: calc(var(--line-height) / 2); + width: calc(round(down, 100%, 1ch)); + border-collapse: collapse; + margin: 0 0 calc(var(--line-height) * 2); +} + +th, td { + border: var(--border-thickness) solid var(--text-color); + padding: + calc((var(--line-height) / 2)) + calc(1ch - var(--border-thickness) / 2) + calc((var(--line-height) / 2) - (var(--border-thickness))) + ; + line-height: var(--line-height); + vertical-align: top; + text-align: left; +} +table tbody tr:first-child > * { + padding-top: calc((var(--line-height) / 2) - var(--border-thickness)); +} + + +th { + font-weight: 700; +} +.width-min { + width: 0%; +} +.width-auto { + width: 100%; +} + +.header { + margin-bottom: calc(var(--line-height) * 2); +} +.header h1 { + margin: 0; +} +.header tr td:last-child { + text-align: right; +} + +p { + word-break: break-word; + word-wrap: break-word; + hyphens: auto; +} + +img, video { + display: block; + width: 100%; + object-fit: contain; + overflow: hidden; +} +img { + font-style: italic; + color: var(--text-color-alt); +} + +details { + border: var(--border-thickness) solid var(--text-color); + padding: calc(var(--line-height) - var(--border-thickness)) 1ch; + margin-bottom: var(--line-height); +} + +summary { + font-weight: var(--font-weight-medium); + cursor: pointer; +} +details[open] summary { + margin-bottom: var(--line-height); +} + +details ::marker { + display: inline-block; + content: '▶'; + margin: 0; +} +details[open] ::marker { + content: '▼'; +} + +details :last-child { + margin-bottom: 0; +} + +pre { + white-space: pre; + overflow-x: auto; + margin: var(--line-height) 0; + overflow-y: hidden; +} +figure pre { + margin: 0; +} + +pre, code { + font-family: var(--font-family); +} + +code { + font-weight: var(--font-weight-medium); +} + +figure { + margin: calc(var(--line-height) * 2) 3ch; + overflow-x: auto; + overflow-y: hidden; +} + +figcaption { + display: block; + font-style: italic; + margin-top: var(--line-height); +} + +ul, ol { + padding: 0; + margin: 0 0 var(--line-height); +} + +ul { + list-style-type: square; + padding: 0 0 0 2ch; +} +ol { + list-style-type: none; + counter-reset: item; + padding: 0; +} +ol ul, +ol ol, +ul ol, +ul ul { + padding: 0 0 0 3ch; + margin: 0; +} +ol li:before { + content: counters(item, ".") ". "; + counter-increment: item; + font-weight: var(--font-weight-medium); +} + +li { + margin: 0; + padding: 0; +} + +li::marker { + line-height: 0; +} + +::-webkit-scrollbar { + height: var(--line-height); +} + +input, button, textarea { + border: var(--border-thickness) solid var(--text-color); + padding: + calc(var(--line-height) / 2 - var(--border-thickness)) + calc(1ch - var(--border-thickness)); + margin: 0; + font: inherit; + font-weight: inherit; + height: calc(var(--line-height) * 2); + width: auto; + overflow: visible; + background: var(--background-color); + color: var(--text-color); + line-height: normal; + -webkit-font-smoothing: inherit; + -moz-osx-font-smoothing: inherit; + -webkit-appearance: none; +} + +input[type=checkbox], +input[type=radio] { + display: inline-grid; + place-content: center; + vertical-align: top; + width: 2ch; + height: var(--line-height); + cursor: pointer; +} +input[type=checkbox]:checked:before, +input[type=radio]:checked:before { + content: ""; + width: 1ch; + height: calc(var(--line-height) / 2); + background: var(--text-color); +} +input[type=radio], +input[type=radio]:before { + border-radius: 100%; +} + +button:focus, input:focus { + --border-thickness: 3px; + outline: none; +} + +input { + width: calc(round(down, 100%, 1ch)); +} +::placeholder { + color: var(--text-color-alt); + opacity: 1; +} +::-ms-input-placeholder { + color: var(--text-color-alt); +} +button::-moz-focus-inner { + padding: 0; + border: 0 +} + +button { + text-transform: uppercase; + font-weight: var(--font-weight-medium); + cursor: pointer; +} + +button:hover { + background: var(--background-color-alt); +} +button:active { + transform: translate(2px, 2px); +} + +label { + display: block; + width: calc(round(down, 100%, 1ch)); + height: auto; + line-height: var(--line-height); + font-weight: var(--font-weight-medium); + margin: 0; +} + +label input { + width: 100%; +} + +.tree, .tree ul { + position: relative; + padding-left: 0; + list-style-type: none; + line-height: var(--line-height); +} +.tree ul { + margin: 0; +} +.tree ul li { + position: relative; + padding-left: 1.5ch; + margin-left: 1.5ch; + border-left: var(--border-thickness) solid var(--text-color); +} +.tree ul li:before { + position: absolute; + display: block; + top: calc(var(--line-height) / 2); + left: 0; + content: ""; + width: 1ch; + border-bottom: var(--border-thickness) solid var(--text-color); +} +.tree ul li:last-child { + border-left: none; +} +.tree ul li:last-child:after { + position: absolute; + display: block; + top: 0; + left: 0; + content: ""; + height: calc(var(--line-height) / 2); + border-left: var(--border-thickness) solid var(--text-color); +} + +.grid { + --grid-cells: 0; + display: flex; + gap: 1ch; + width: calc(round(down, 100%, (1ch * var(--grid-cells)) - (1ch * var(--grid-cells) - 1))); + margin-bottom: var(--line-height); +} + +.grid > *, +.grid > input { + flex: 0 0 calc(round(down, (100% - (1ch * (var(--grid-cells) - 1))) / var(--grid-cells), 1ch)); +} +.grid:has(> :last-child:nth-child(1)) { --grid-cells: 1; } +.grid:has(> :last-child:nth-child(2)) { --grid-cells: 2; } +.grid:has(> :last-child:nth-child(3)) { --grid-cells: 3; } +.grid:has(> :last-child:nth-child(4)) { --grid-cells: 4; } +.grid:has(> :last-child:nth-child(5)) { --grid-cells: 5; } +.grid:has(> :last-child:nth-child(6)) { --grid-cells: 6; } +.grid:has(> :last-child:nth-child(7)) { --grid-cells: 7; } +.grid:has(> :last-child:nth-child(8)) { --grid-cells: 8; } +.grid:has(> :last-child:nth-child(9)) { --grid-cells: 9; } + +/* DEBUG UTILITIES */ + +.debug .debug-grid { + --color: color-mix(in srgb, var(--text-color) 10%, var(--background-color) 90%); + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -1; + background-image: + repeating-linear-gradient(var(--color) 0 1px, transparent 1px 100%), + repeating-linear-gradient(90deg, var(--color) 0 1px, transparent 1px 100%); + background-size: 1ch var(--line-height); + margin: 0; +} + +.debug .off-grid { + background: rgba(255, 0, 0, 0.1); +} + +.debug-toggle-label { + text-align: right; +} diff --git a/static/css/reset.css b/static/css/reset.css new file mode 100644 index 0000000..aabd759 --- /dev/null +++ b/static/css/reset.css @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + /*margin: 0;*/ + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/static/js/index.js b/static/js/index.js new file mode 100644 index 0000000..4379fc2 --- /dev/null +++ b/static/js/index.js @@ -0,0 +1,120 @@ +function gridCellDimensions() { + const element = document.createElement("div"); + element.style.position = "fixed"; + element.style.height = "var(--line-height)"; + element.style.width = "1ch"; + document.body.appendChild(element); + const rect = element.getBoundingClientRect(); + document.body.removeChild(element); + return { width: rect.width, height: rect.height }; +} + +// Add padding to each media to maintain grid. +function adjustMediaPadding() { + const cell = gridCellDimensions(); + + function setHeightFromRatio(media, ratio) { + const rect = media.getBoundingClientRect(); + const realHeight = rect.width / ratio; + const diff = cell.height - (realHeight % cell.height); + media.style.setProperty("padding-bottom", `${diff}px`); + } + + function setFallbackHeight(media) { + const rect = media.getBoundingClientRect(); + const height = Math.round((rect.width / 2) / cell.height) * cell.height; + media.style.setProperty("height", `${height}px`); + } + + function onMediaLoaded(media) { + var width, height; + switch (media.tagName) { + case "IMG": + width = media.naturalWidth; + height = media.naturalHeight; + break; + case "VIDEO": + width = media.videoWidth; + height = media.videoHeight; + break; + } + if (width > 0 && height > 0) { + setHeightFromRatio(media, width / height); + } else { + setFallbackHeight(media); + } + } + + const medias = document.querySelectorAll("img, video"); + for (media of medias) { + switch (media.tagName) { + case "IMG": + if (media.complete) { + onMediaLoaded(media); + } else { + media.addEventListener("load", () => onMediaLoaded(media)); + media.addEventListener("error", function() { + setFallbackHeight(media); + }); + } + break; + case "VIDEO": + switch (media.readyState) { + case HTMLMediaElement.HAVE_CURRENT_DATA: + case HTMLMediaElement.HAVE_FUTURE_DATA: + case HTMLMediaElement.HAVE_ENOUGH_DATA: + onMediaLoaded(media); + break; + default: + media.addEventListener("loadeddata", () => onMediaLoaded(media)); + media.addEventListener("error", function() { + setFallbackHeight(media); + }); + break; + } + break; + } + } +} + +adjustMediaPadding(); +window.addEventListener("load", adjustMediaPadding); +window.addEventListener("resize", adjustMediaPadding); + +function checkOffsets() { + const ignoredTagNames = new Set([ + "THEAD", + "TBODY", + "TFOOT", + "TR", + "TD", + "TH", + ]); + const cell = gridCellDimensions(); + const elements = document.querySelectorAll("body :not(.debug-grid, .debug-toggle)"); + for (const element of elements) { + if (ignoredTagNames.has(element.tagName)) { + continue; + } + const rect = element.getBoundingClientRect(); + if (rect.width === 0 && rect.height === 0) { + continue; + } + const top = rect.top + window.scrollY; + const left = rect.left + window.scrollX; + const offset = top % (cell.height / 2); + if(offset > 0) { + element.classList.add("off-grid"); + console.error("Incorrect vertical offset for", element, "with remainder", top % cell.height, "when expecting divisible by", cell.height / 2); + } else { + element.classList.remove("off-grid"); + } + } +} + +const debugToggle = document.querySelector(".debug-toggle"); +function onDebugToggle() { + document.body.classList.toggle("debug", debugToggle.checked); +} +debugToggle.addEventListener("change", onDebugToggle); +onDebugToggle();