First Commit
This commit is contained in:
commit
923d6ca242
35 changed files with 4933 additions and 0 deletions
126
.gitignore
vendored
Normal file
126
.gitignore
vendored
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# .gitignore
|
||||||
|
# Bare Minimum Git
|
||||||
|
# https://salferrarello.com/starter-gitignore-file/
|
||||||
|
# ver 20221125
|
||||||
|
#
|
||||||
|
# From the root of your project run
|
||||||
|
# curl -O https://gist.githubusercontent.com/salcode/10017553/raw/.gitignore
|
||||||
|
# to download this file
|
||||||
|
#
|
||||||
|
# This file is tailored for a general web project, it
|
||||||
|
# is NOT optimized for a WordPress project. See
|
||||||
|
# https://gist.github.com/salcode/b515f520d3f8207ecd04
|
||||||
|
# for a WordPress specific .gitignore
|
||||||
|
#
|
||||||
|
# This file specifies intentionally untracked files to ignore
|
||||||
|
# http://git-scm.com/docs/gitignore
|
||||||
|
#
|
||||||
|
# NOTES:
|
||||||
|
# The purpose of gitignore files is to ensure that certain files not
|
||||||
|
# tracked by Git remain untracked.
|
||||||
|
#
|
||||||
|
# To ignore uncommitted changes in a file that is already tracked,
|
||||||
|
# use `git update-index --assume-unchanged`.
|
||||||
|
#
|
||||||
|
# To stop tracking a file that is currently tracked,
|
||||||
|
# use `git rm --cached`
|
||||||
|
#
|
||||||
|
# Change Log:
|
||||||
|
# 20221125 ignore /dist directory
|
||||||
|
# unignore /.git-blame-ignore-revs
|
||||||
|
# 20220720 ignore /build directory
|
||||||
|
# 20220128 unignore .nvmrc
|
||||||
|
# 20210211 unignore .env.example
|
||||||
|
# 20190705 ignore private/secret files
|
||||||
|
# 20181206 remove trailing whitespaces
|
||||||
|
# 20180714 unignore .phpcs.xml.dist
|
||||||
|
# 20170502 unignore composer.lock
|
||||||
|
# 20170502 ignore components loaded via Bower
|
||||||
|
# 20150326 ignore jekyll build directory `/_site`
|
||||||
|
# 20150324 Reorganized file to list ignores first and whitelisted last,
|
||||||
|
# change WordPress .gitignore link to preferred gist,
|
||||||
|
# add curl line for quick installation
|
||||||
|
# ignore composer files (vendor directory and lock file)
|
||||||
|
# 20140606 Add .editorconfig as a tracked file
|
||||||
|
# 20140418 remove explicit inclusion
|
||||||
|
# of readme.md (this is not an ignored file by default)
|
||||||
|
# 20140407 Initially Published
|
||||||
|
#
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
|
||||||
|
# ignore all files starting with . or ~
|
||||||
|
.*
|
||||||
|
~*
|
||||||
|
|
||||||
|
# ignore node/grunt dependency directories
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Ignore build directories.
|
||||||
|
/build
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# ignore composer vendor directory
|
||||||
|
/vendor
|
||||||
|
|
||||||
|
# ignore components loaded via Bower
|
||||||
|
/bower_components
|
||||||
|
|
||||||
|
# ignore jekyll build directory
|
||||||
|
/_site
|
||||||
|
|
||||||
|
# ignore OS generated files
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# ignore Editor files
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
*.komodoproject
|
||||||
|
|
||||||
|
# ignore log files and databases
|
||||||
|
*.log
|
||||||
|
*.sql
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
|
# ignore compiled files
|
||||||
|
*.com
|
||||||
|
*.class
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.o
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# ignore packaged files
|
||||||
|
*.7z
|
||||||
|
*.dmg
|
||||||
|
*.gz
|
||||||
|
*.iso
|
||||||
|
*.jar
|
||||||
|
*.rar
|
||||||
|
*.tar
|
||||||
|
*.zip
|
||||||
|
|
||||||
|
# ignore private/secret files
|
||||||
|
*.der
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# BEGIN Explictly Allowed Files (i.e. do NOT ignore these)
|
||||||
|
# --------------------------------------------------------
|
||||||
|
|
||||||
|
# track these files, if they exist
|
||||||
|
!.editorconfig
|
||||||
|
!.env.example
|
||||||
|
!.git-blame-ignore-revs
|
||||||
|
!.gitignore
|
||||||
|
!.nvmrc
|
||||||
|
!.phpcs.xml.dist
|
||||||
|
|
||||||
|
# ------------
|
||||||
|
# USER CHANGES
|
||||||
|
# ------------
|
||||||
|
|
||||||
|
# ignore public directory (we build it on server)
|
||||||
|
/public
|
17
composer.json
Normal file
17
composer.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"GamesShop\\": "src/php"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"league/container": "^4.2",
|
||||||
|
"league/plates": "^3.5",
|
||||||
|
"league/route": "^5.1",
|
||||||
|
"filp/whoops": "^2.15",
|
||||||
|
"laminas/laminas-diactoros": "^3.3",
|
||||||
|
"laminas/laminas-httphandlerrunner": "^2.10",
|
||||||
|
"ralouphie/mimey": "^1.0",
|
||||||
|
"dotenv-org/phpdotenv-vault": "^0.2.4"
|
||||||
|
}
|
||||||
|
}
|
1524
composer.lock
generated
Normal file
1524
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
0
config/.env.example
Normal file
0
config/.env.example
Normal file
13
deploy/Vagrantfile
vendored
Normal file
13
deploy/Vagrantfile
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
Vagrant.configure("2") do |config|
|
||||||
|
|
||||||
|
config.vm.box = "vagrant-php83-mysql"
|
||||||
|
|
||||||
|
config.vm.network "forwarded_port", guest: 80, host: 8080
|
||||||
|
|
||||||
|
config.vm.synced_folder "../../games-shop", "/var/www/html"
|
||||||
|
|
||||||
|
config.vm.provision "shell", inline: <<-SHELL
|
||||||
|
sudo rm /etc/nginx/sites-enabled/default
|
||||||
|
sudo ln -s /var/www/html/deploy/nginx-file /etc/nginx/sites-enabled/default
|
||||||
|
SHELL
|
||||||
|
end
|
11
deploy/nginx-file
Normal file
11
deploy/nginx-file
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
|
||||||
|
root /var/www/html/public;
|
||||||
|
location / {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
|
||||||
|
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
|
||||||
|
}
|
||||||
|
}
|
2547
package-lock.json
generated
Normal file
2547
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
31
package.json
Normal file
31
package.json
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "games-shop",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"private": "true",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "npx webpack",
|
||||||
|
"watch": "npx webpack --watch"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||||
|
"@types/bootstrap": "^5.2.10",
|
||||||
|
"bootstrap": "^5.3.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"assets-webpack-plugin": "^7.1.1",
|
||||||
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
|
"css-loader": "^7.1.2",
|
||||||
|
"mini-css-extract-plugin": "^2.9.0",
|
||||||
|
"resolve-url-loader": "^5.0.0",
|
||||||
|
"sass": "^1.77.5",
|
||||||
|
"sass-loader": "^14.2.1",
|
||||||
|
"ts-loader": "^9.5.1",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"webpack": "^5.92.0",
|
||||||
|
"webpack-cli": "^5.1.4"
|
||||||
|
}
|
||||||
|
}
|
3
src/css/common/fonts.scss
Normal file
3
src/css/common/fonts.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
@import "@fortawesome/fontawesome-free/scss/fontawesome";
|
||||||
|
@import "@fortawesome/fontawesome-free/scss/solid";
|
||||||
|
@import "@fortawesome/fontawesome-free/scss/brands";
|
13
src/css/common/index.scss
Normal file
13
src/css/common/index.scss
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
@import "bootstrap";
|
||||||
|
@import "fonts";
|
||||||
|
|
||||||
|
$main-container-max-width: 992px;
|
||||||
|
|
||||||
|
.navbar :first-child,
|
||||||
|
main {
|
||||||
|
width: 100%;
|
||||||
|
max-width: $main-container-max-width;
|
||||||
|
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
1
src/file-index.json
Normal file
1
src/file-index.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"index":{"js":["js/runtime.js","js/index.js"],"css":"css/common.css"}}
|
1
src/js/common/index.ts
Normal file
1
src/js/common/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import "./theme";
|
39
src/js/common/theme.ts
Normal file
39
src/js/common/theme.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
(() => {
|
||||||
|
function setTheme (mode = 'auto') {
|
||||||
|
const userMode = localStorage.getItem('bs-theme');
|
||||||
|
const sysMode = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||||
|
const useSystem = mode === 'system' || (!userMode && mode === 'auto');
|
||||||
|
|
||||||
|
const modeChosen = selectMode(mode);
|
||||||
|
|
||||||
|
if (useSystem) {
|
||||||
|
localStorage.removeItem('bs-theme');
|
||||||
|
} else {
|
||||||
|
localStorage.setItem('bs-theme', modeChosen);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', useSystem ? (sysMode ? 'light' : 'dark') : modeChosen);
|
||||||
|
document.querySelectorAll('.mode-switch .btn').forEach(e => e.classList.remove('text-body'));
|
||||||
|
document.getElementById(modeChosen)?.classList.add('text-body');
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectMode(mode: string): string {
|
||||||
|
const userMode = <string>localStorage.getItem('bs-theme');
|
||||||
|
const useSystem = mode === 'system' || (!userMode && mode === 'auto');
|
||||||
|
|
||||||
|
if (useSystem) {
|
||||||
|
return 'system';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'dark' || mode === 'light') {
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
return userMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
setTheme();
|
||||||
|
document.querySelectorAll('.mode-switch .btn').forEach(e => e.addEventListener('click', () => setTheme(e.id)));
|
||||||
|
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => setTheme());
|
||||||
|
});
|
||||||
|
})()
|
2
src/js/pages/index.ts
Normal file
2
src/js/pages/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import '../../css/common/index.scss';
|
||||||
|
import '../common/index';
|
38
src/php/ContainerHandler.php
Normal file
38
src/php/ContainerHandler.php
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace GamesShop;
|
||||||
|
|
||||||
|
use League\Container\Container;
|
||||||
|
use League\Container\ReflectionContainer;
|
||||||
|
|
||||||
|
final class ContainerHandler
|
||||||
|
{
|
||||||
|
private static Container|null $instance = null;
|
||||||
|
|
||||||
|
public static function getInstance(): Container
|
||||||
|
{
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::createInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template RequestedType
|
||||||
|
*
|
||||||
|
* @param class-string<RequestedType>|string $id
|
||||||
|
*
|
||||||
|
* @return RequestedType|mixed
|
||||||
|
*/
|
||||||
|
public static function get(string $id) {
|
||||||
|
return self::getInstance()->get($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function createInstance()
|
||||||
|
{
|
||||||
|
self::$instance = new Container();
|
||||||
|
self::$instance->delegate(new ReflectionContainer(true));
|
||||||
|
}
|
||||||
|
}
|
13
src/php/Environment/DiscordEnvironment.php
Normal file
13
src/php/Environment/DiscordEnvironment.php
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace GamesShop\Environment;
|
||||||
|
|
||||||
|
final readonly class DiscordEnvironment
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $clientId,
|
||||||
|
public string $clientSecret,
|
||||||
|
public string $loginUrl,
|
||||||
|
) {}
|
||||||
|
}
|
28
src/php/Environment/EnvironmentHandler.php
Normal file
28
src/php/Environment/EnvironmentHandler.php
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace GamesShop\Environment;
|
||||||
|
|
||||||
|
use DotenvVault\DotenvVault;
|
||||||
|
use GamesShop\Paths;
|
||||||
|
|
||||||
|
final class EnvironmentHandler
|
||||||
|
{
|
||||||
|
private const string ENVIRONMENT_PATH = Paths::ROOT_PATH . '/config';
|
||||||
|
|
||||||
|
public function load() {
|
||||||
|
$dotEnv = DotenvVault::createImmutable(
|
||||||
|
self::ENVIRONMENT_PATH
|
||||||
|
);
|
||||||
|
|
||||||
|
$dotEnv->safeLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDiscordEnvironment(): DiscordEnvironment {
|
||||||
|
return new DiscordEnvironment(
|
||||||
|
$_SERVER['DISCORD_CLIENT_ID'],
|
||||||
|
$_SERVER['DISCORD_CLIENT_SECRET'],
|
||||||
|
$_SERVER['DISCORD_CLIENT_LOGIN_URI'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
17
src/php/Login/LoginHandler.php
Normal file
17
src/php/Login/LoginHandler.php
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace GamesShop\Login;
|
||||||
|
|
||||||
|
final class LoginHandler
|
||||||
|
{
|
||||||
|
public function isLoggedIn(): bool {
|
||||||
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
return isset($_SESSION['accountid']);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
11
src/php/Paths.php
Normal file
11
src/php/Paths.php
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace GamesShop;
|
||||||
|
|
||||||
|
final class Paths
|
||||||
|
{
|
||||||
|
public const string ROOT_PATH = __DIR__ . '/../..';
|
||||||
|
public const string PUBLIC_PATH = self::ROOT_PATH . '/public';
|
||||||
|
public const string SOURCE_PATH = self::ROOT_PATH . '/src';
|
||||||
|
}
|
20
src/php/Routing/ErrorRoute.php
Normal file
20
src/php/Routing/ErrorRoute.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace GamesShop\Routing;
|
||||||
|
|
||||||
|
use GamesShop\ContainerHandler;
|
||||||
|
use GamesShop\Templates\TemplateEngine;
|
||||||
|
use Laminas\Diactoros\Response;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
|
final class ErrorRoute
|
||||||
|
{
|
||||||
|
public function renderErrorPage(int $errorCode): ResponseInterface {
|
||||||
|
$pageContent = ContainerHandler::get(TemplateEngine::class)->renderPage('error', [ 'errorCode' => $errorCode ]);
|
||||||
|
|
||||||
|
$response = new Response;
|
||||||
|
$response->getBody()->write($pageContent);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
33
src/php/Routing/IndexRoute.php
Normal file
33
src/php/Routing/IndexRoute.php
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace GamesShop\Routing;
|
||||||
|
|
||||||
|
use GamesShop\ContainerHandler;
|
||||||
|
use GamesShop\Login\LoginHandler;
|
||||||
|
use GamesShop\Templates\TemplateEngine;
|
||||||
|
use Laminas\Diactoros\Response;
|
||||||
|
use Laminas\Diactoros\Response\RedirectResponse;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
final class IndexRoute
|
||||||
|
{
|
||||||
|
public function __invoke(ServerRequestInterface $request): ResponseInterface {
|
||||||
|
$loginHandler = ContainerHandler::get(LoginHandler::class);
|
||||||
|
|
||||||
|
if (!$loginHandler->isLoggedIn()) {
|
||||||
|
return new RedirectResponse('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageContent = ContainerHandler::get(TemplateEngine::class)->renderPage('index');
|
||||||
|
|
||||||
|
$response = new Response;
|
||||||
|
$response->getBody()->write($pageContent);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function applyRoutes(\League\Route\Router $router): void {
|
||||||
|
$router->get('/', self::class);
|
||||||
|
}
|
||||||
|
}
|
33
src/php/Routing/LoginRoutes.php
Normal file
33
src/php/Routing/LoginRoutes.php
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace GamesShop\Routing;
|
||||||
|
|
||||||
|
use GamesShop\ContainerHandler;
|
||||||
|
use GamesShop\Environment\EnvironmentHandler;
|
||||||
|
use GamesShop\Templates\TemplateEngine;
|
||||||
|
use Laminas\Diactoros\Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
final class LoginRoutes
|
||||||
|
{
|
||||||
|
public function login(ServerRequestInterface $request) {
|
||||||
|
$discordEnv = ContainerHandler::get(EnvironmentHandler::class)->getDiscordEnvironment();
|
||||||
|
$pageContent = ContainerHandler::get(TemplateEngine::class)->renderPage(
|
||||||
|
'login',
|
||||||
|
[
|
||||||
|
'discordUrl' => $discordEnv->loginUrl
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = new Response;
|
||||||
|
$response->getBody()->write($pageContent);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function addRoutes(\League\Route\Router $router): void {
|
||||||
|
$routes = ContainerHandler::get(LoginRoutes::class);
|
||||||
|
|
||||||
|
$router->get('/login', $routes->login(...));
|
||||||
|
}
|
||||||
|
}
|
51
src/php/Routing/ResourceRoute.php
Normal file
51
src/php/Routing/ResourceRoute.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace GamesShop\Routing;
|
||||||
|
|
||||||
|
use GamesShop\ContainerHandler;
|
||||||
|
use GamesShop\Paths;
|
||||||
|
use Laminas\Diactoros\Response;
|
||||||
|
use Mimey\MimeTypes;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
final class ResourceRoute
|
||||||
|
{
|
||||||
|
private const array RESOURCE_EXTENSIONS = [
|
||||||
|
'js',
|
||||||
|
'css',
|
||||||
|
'ttf', 'woff', 'woff2',
|
||||||
|
'gif', 'svg', 'png', 'jpg'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __invoke(ServerRequestInterface $request, array $args): ResponseInterface {
|
||||||
|
|
||||||
|
$filePath = Paths::PUBLIC_PATH . $request->getUri()->getPath();
|
||||||
|
|
||||||
|
if (!file_exists($filePath)) {
|
||||||
|
$response = new Response(status: 404);
|
||||||
|
$response->getBody()->write('File not found');
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mimey = ContainerHandler::get(MimeTypes::class);
|
||||||
|
$response = new Response(
|
||||||
|
headers: [
|
||||||
|
'Content-Type' => $mimey->getMimeType(pathinfo($filePath, PATHINFO_EXTENSION)),
|
||||||
|
'Cache-Control' => 'public, max-age=3600, must-revalidate',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$response->getBody()->write(file_get_contents($filePath));
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function addRouteEntry(\League\Route\Router $router): void {
|
||||||
|
$joinedResourceExtensions = implode('|', self::RESOURCE_EXTENSIONS);
|
||||||
|
$router->addPatternMatcher('resource', ".+[{$joinedResourceExtensions}]");
|
||||||
|
|
||||||
|
$router->get('/{resource:resource}', self::class);
|
||||||
|
}
|
||||||
|
}
|
37
src/php/Routing/Router.php
Normal file
37
src/php/Routing/Router.php
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace GamesShop\Routing;
|
||||||
|
|
||||||
|
use GamesShop\ContainerHandler;
|
||||||
|
use GamesShop\Login\LoginHandler;
|
||||||
|
use GamesShop\Templates\TemplateEngine;
|
||||||
|
use Laminas\Diactoros\Response;
|
||||||
|
use Laminas\Diactoros\ServerRequestFactory;
|
||||||
|
use League\Container\Container;
|
||||||
|
use League\Route\Strategy\ApplicationStrategy;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
final class Router
|
||||||
|
{
|
||||||
|
public function route()
|
||||||
|
{
|
||||||
|
$request = ServerRequestFactory::fromGlobals(
|
||||||
|
$_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
|
||||||
|
);
|
||||||
|
$router = new \League\Route\Router;
|
||||||
|
$strategy = (new ApplicationStrategy)->setContainer(ContainerHandler::getInstance());
|
||||||
|
$router->setStrategy($strategy);
|
||||||
|
|
||||||
|
IndexRoute::applyRoutes($router);
|
||||||
|
LoginRoutes::addRoutes($router);
|
||||||
|
ResourceRoute::addRouteEntry($router);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $router->dispatch($request);
|
||||||
|
} catch (\League\Route\Http\Exception\NotFoundException $e) {
|
||||||
|
return (new ErrorRoute())->renderErrorPage(404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
src/php/Templates/ResourceEntry.php
Normal file
16
src/php/Templates/ResourceEntry.php
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace GamesShop\Templates;
|
||||||
|
|
||||||
|
final class ResourceEntry
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string[] $js
|
||||||
|
* @param string[] $css
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public readonly array $js,
|
||||||
|
public readonly array $css,
|
||||||
|
) { }
|
||||||
|
}
|
51
src/php/Templates/ResourceIndex.php
Normal file
51
src/php/Templates/ResourceIndex.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace GamesShop\Templates;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use GamesShop\Paths;
|
||||||
|
|
||||||
|
final class ResourceIndex
|
||||||
|
{
|
||||||
|
private const string PATH = Paths::SOURCE_PATH . '/file-index.json';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ResourceEntry[]
|
||||||
|
*/
|
||||||
|
private array $resources = [];
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$fileContents = file_get_contents(self::PATH);
|
||||||
|
$index = json_decode($fileContents, true);
|
||||||
|
|
||||||
|
foreach ($index as $entryKey => $resource) {
|
||||||
|
$js = $resource['js'];
|
||||||
|
if (is_string($js)) {
|
||||||
|
$js = [$js];
|
||||||
|
}
|
||||||
|
|
||||||
|
$css = $resource['css'];
|
||||||
|
if (is_string($css)) {
|
||||||
|
$css = [$css];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->resources[$entryKey] = new ResourceEntry(
|
||||||
|
$js, $css
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function getResource(string $entry): ResourceEntry
|
||||||
|
{
|
||||||
|
if (!array_key_exists($entry, $this->resources)) {
|
||||||
|
throw new Exception("Entry '$entry' not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->resources[$entry];
|
||||||
|
}
|
||||||
|
}
|
27
src/php/Templates/TemplateEngine.php
Normal file
27
src/php/Templates/TemplateEngine.php
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace GamesShop\Templates;
|
||||||
|
|
||||||
|
use GamesShop\Paths;
|
||||||
|
use League\Plates\Engine;
|
||||||
|
|
||||||
|
final class TemplateEngine extends Engine
|
||||||
|
{
|
||||||
|
private const string TEMPLATES_PATH = Paths::SOURCE_PATH . '/templates';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private ResourceIndex $resourceIndex,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
parent::__construct(self::TEMPLATES_PATH, 'php');
|
||||||
|
$this->addData([
|
||||||
|
'resources' => $this->resourceIndex,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderPage(string $page, array $data = array())
|
||||||
|
{
|
||||||
|
return parent::render("pages/$page", $data);
|
||||||
|
}
|
||||||
|
}
|
22
src/php/index.dev.php
Normal file
22
src/php/index.dev.php
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use GamesShop\ContainerHandler;
|
||||||
|
use GamesShop\Environment\EnvironmentHandler;
|
||||||
|
use GamesShop\Routing\Router;
|
||||||
|
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
|
||||||
|
use Whoops\Handler\PrettyPageHandler;
|
||||||
|
use Whoops\Run;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
ContainerHandler::get(EnvironmentHandler::class)->load();
|
||||||
|
|
||||||
|
$whoops = new Run();
|
||||||
|
$whoops->pushHandler(new PrettyPageHandler);
|
||||||
|
$whoops->register();
|
||||||
|
|
||||||
|
$router = ContainerHandler::getInstance()->get(Router::class);
|
||||||
|
$result = $router->route();
|
||||||
|
|
||||||
|
(new SapiEmitter)->emit($result);
|
2
src/php/index.prod.php
Normal file
2
src/php/index.prod.php
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
66
src/templates/layout/main.php
Normal file
66
src/templates/layout/main.php
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use GamesShop\Templates\ResourceIndex;
|
||||||
|
use League\Plates\Template\Template;
|
||||||
|
|
||||||
|
assert($this instanceof Template);
|
||||||
|
/** @var ResourceIndex $resources */
|
||||||
|
assert($resources instanceof ResourceIndex);
|
||||||
|
|
||||||
|
if (!isset($resourceEntry)) {
|
||||||
|
throw new Exception("Resource entry not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string $resourceEntry
|
||||||
|
*/
|
||||||
|
|
||||||
|
$resource = $resources->getResource($resourceEntry);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Game Shop</title>
|
||||||
|
|
||||||
|
<?php foreach ($resource->js as $js): ?>
|
||||||
|
<script src="/<?= $js ?>"></script>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php foreach ($resource->css as $css): ?>
|
||||||
|
<link rel="stylesheet" href="/<?= $css ?>">
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</head>
|
||||||
|
<body class="vh-100 d-flex flex-column">
|
||||||
|
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/">Game Shop</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-content" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbar-content">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0"></ul>
|
||||||
|
|
||||||
|
<div class="d-flex mode-switch">
|
||||||
|
<button title="Use dark mode" id="dark" class="btn btn-sm btn-default text-secondary">
|
||||||
|
<i class="fa-regular fa-moon"></i>
|
||||||
|
</button>
|
||||||
|
<button title="Use light mode" id="light" class="btn btn-sm btn-default text-secondary">
|
||||||
|
<i class="fa-regular fa-sun"></i>
|
||||||
|
</button>
|
||||||
|
<button title="Use system preferred mode" id="system" class="btn btn-sm btn-default text-secondary">
|
||||||
|
<i class="fa-solid fa-display"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="mt-2 position-relative flex-grow-1">
|
||||||
|
<?= $this->section('content'); ?>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
11
src/templates/pages/error.php
Normal file
11
src/templates/pages/error.php
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$this->layout('layout/main', [ 'resourceEntry' => 'index' ]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int $errorCode
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
|
||||||
|
<img src="https://http.dog/<?= $errorCode ?>.jpg" alt="Error <?= $errorCode ?>" class="position-absolute top-50 start-50 translate-middle w-100" style="max-width: 800px" />
|
9
src/templates/pages/index.php
Normal file
9
src/templates/pages/index.php
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$this->layout('layout/main', [ 'resourceEntry' => 'index' ]);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<h1>
|
||||||
|
Hello
|
||||||
|
</h1>
|
15
src/templates/pages/login.php
Normal file
15
src/templates/pages/login.php
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$this->layout('layout/main', [ 'resourceEntry' => 'index' ]);
|
||||||
|
|
||||||
|
/** @var string $discordUrl */
|
||||||
|
?>
|
||||||
|
|
||||||
|
<a href="<?= $discordUrl ?>">
|
||||||
|
<button class="btn btn-primary position-absolute top-50 start-50 translate-middle">
|
||||||
|
<i class="fa-brands fa-discord"></i>
|
||||||
|
Login with Discord
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</a>
|
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2016",
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
}
|
||||||
|
}
|
95
webpack.config.js
Normal file
95
webpack.config.js
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin'),
|
||||||
|
Path = require('path'),
|
||||||
|
AssetsPlugin = require('assets-webpack-plugin'),
|
||||||
|
CopyPlugin = require('copy-webpack-plugin');
|
||||||
|
|
||||||
|
const PUBLIC_FOLDER = Path.resolve(__dirname, 'public'),
|
||||||
|
SOURCE_FOLDER = Path.resolve(__dirname, 'src'),
|
||||||
|
JS_FOLDER = Path.resolve(SOURCE_FOLDER, 'js'),
|
||||||
|
CSS_FOLDER = Path.resolve(SOURCE_FOLDER, 'css'),
|
||||||
|
PHP_FOLDER = Path.resolve(SOURCE_FOLDER, 'php');
|
||||||
|
|
||||||
|
const PROD = false;
|
||||||
|
|
||||||
|
const INDEX_PATH = Path.resolve(PHP_FOLDER, PROD ? 'index.prod.php' : 'index.dev.php');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
new AssetsPlugin({
|
||||||
|
filename: 'file-index.json',
|
||||||
|
path: SOURCE_FOLDER,
|
||||||
|
includeAllFileTypes: false,
|
||||||
|
entrypoints: true,
|
||||||
|
removeFullPathAutoPrefix: true
|
||||||
|
}),
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: 'css/[name].css'
|
||||||
|
}),
|
||||||
|
new CopyPlugin({
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
from: INDEX_PATH,
|
||||||
|
to: PUBLIC_FOLDER + '/index.php'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
mode: PROD ? 'production' : 'development',
|
||||||
|
devtool: 'source-map',
|
||||||
|
optimization: {
|
||||||
|
runtimeChunk: "single",
|
||||||
|
splitChunks: {
|
||||||
|
cacheGroups: {
|
||||||
|
vendor: {
|
||||||
|
test: /[\\/]node_modules[\\/]/,
|
||||||
|
name: 'vendors',
|
||||||
|
chunks: 'all',
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
test: /[\\/]common[\\/]/,
|
||||||
|
name: 'common',
|
||||||
|
chunks: 'all'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.css$/i,
|
||||||
|
use: [
|
||||||
|
MiniCssExtractPlugin.loader,
|
||||||
|
'css-loader'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.scss$/i,
|
||||||
|
use: [
|
||||||
|
MiniCssExtractPlugin.loader,
|
||||||
|
'css-loader',
|
||||||
|
'resolve-url-loader',
|
||||||
|
"sass-loader"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.ts$/i,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(woff|woff2|eot|ttf|otf|gif|svg)$/i,
|
||||||
|
type: 'asset/resource',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.ts'],
|
||||||
|
},
|
||||||
|
entry: {
|
||||||
|
index: JS_FOLDER + "/pages/index"
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: PUBLIC_FOLDER,
|
||||||
|
filename: 'js/[name].js'
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue