Browse Source

made a start on the more boilerplate kinda stuff

master
Cerys 4 weeks ago
parent
commit
a1dbc0b521
  1. 6
      .gitignore
  2. 44
      App/Configuration.php
  3. 97
      App/TwigExtensions/FiltersExtension.php
  4. 130
      App/TwigExtensions/FunctionExtensions.php
  5. 45
      App/TwigExtensions/NavigationExtension.php
  6. 36
      App/TwigExtensions/StringManipulationExtension.php
  7. 33
      App/TwigTests/TwigTests.php
  8. 18
      App/Wrappers/AlgoliaInteractions.php
  9. 77
      App/Wrappers/DatabaseInteractions.php
  10. 41
      App/Wrappers/SQLQueryBuilderWrapper.php
  11. 15
      Configuration/Configuration.yaml
  12. 0
      LocalStorage/.gitkeep
  13. 0
      LocalStorage/Tunes/.gitkeep
  14. 160
      Localisation/en-GB.yaml
  15. 8
      Pages/index.php
  16. 22
      Pages/tune/uuid.php
  17. 37
      Public/Static/CSS/Elements/HomePage.css
  18. 21
      Public/Static/CSS/Elements/_General.css
  19. 11
      Public/Static/CSS/Mapper.css
  20. 2
      Public/Static/CSS/Themes/Light.css
  21. 30
      Public/Static/JS/AlgoliaInteractions.js
  22. BIN
      Public/favicon.png
  23. 29
      Routing/Router.php
  24. 26
      Templates/Bases/ErrorPage.html.twig
  25. 33
      Templates/Bases/StandardWebPage.html.twig
  26. 4
      Templates/ErrorPages/404.html.twig
  27. 21
      Templates/Pages/index.html.twig
  28. 7
      Templates/Pages/tune/uuid.html.twig
  29. 32
      composer.json
  30. 1326
      composer.lock

6
.gitignore

@ -0,0 +1,6 @@
.idea/
vendor/
composer.phar
Configuration/Secrets.yaml

44
App/Configuration.php

@ -0,0 +1,44 @@
<?php
namespace App;
use App\Common\UUID;
use App\Enumerators\Parameter;
use App\Enumerators\SessionElement;
use App\Wrappers\APIWrapper;
use SimpleXMLElement;
use Symfony\Component\Yaml\Yaml;
class Configuration
{
public static string $SecretDenominator = ";";
public static function GetConfig(string ...$nav) : string|array
{
$config = Yaml::parseFile(__DIR__ . "/../Configuration/Configuration.yaml");
$temp = $config;
foreach ($nav as $n) $temp = $temp[$n];
if(is_array($temp))
{
return $temp;
}
else
{
if(str_starts_with(haystack: $temp, needle: Configuration::$SecretDenominator))
{
$secrets = Yaml::parseFile(__DIR__ . "/../Configuration/Secrets.yaml");
return $secrets[substr($temp, 1)];
}
}
return $temp;
}
public static function GetDictionary() : array
{
return Yaml::parseFile(__DIR__ . "/../Localisation/en-GB.yaml");
}
}

97
App/TwigExtensions/FiltersExtension.php

@ -0,0 +1,97 @@
<?php
namespace App\TwigExtensions;
use App\Configuration;
use DateTime;
use DateTimeZone;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
require_once dirname($_SERVER["DOCUMENT_ROOT"]) . "/vendor/autoload.php";
class FiltersExtension extends AbstractExtension
{
public function getFilters()
{
return [
new TwigFilter('translate', [$this, 'Translate']),
new TwigFilter('base64_encode', [$this, 'Base64Encode']),
new TwigFilter('base64_decode', [$this, 'Base64Decode']),
new TwigFilter('get_type', [$this, 'GetType']),
new TwigFilter('pretty_date', [$this, 'PrettyDate']),
new TwigFilter('pretty_time', [$this, 'PrettyTime']),
new TwigFilter('pretty_date_time', [$this, 'PrettyDateTime']),
new TwigFilter('json_encode', [$this, 'JSONEncode']),
new TwigFilter('json_decode', [$this, 'JSONDecode']),
];
}
public function JSONEncode(mixed $target): bool|string
{
return json_encode(value: $target);
}
public function JSONDecode(string $target): ?array
{
return json_decode($target, true);
}
public function Translate(?string $target): string
{
if($target == null) return "!!$target!!";
if(key_exists(key: $target, array: Configuration::GetDictionary()))
return Configuration::GetDictionary()[$target];
/*
$temp = Configuration::GetDictionary()[$target];
if($temp == null) return "!!$target!!";
return $temp;
*/
return "!!$target!!";
}
public function Base64Encode($target) : string
{
return base64_encode($target) . "==";
}
public function Base64Decode($target)
{
return base64_decode($target);
}
public function GetType($target): string
{
return gettype($target);
}
public function PrettyDate(?string $target): string
{
if($target == "" || $target == null) return "meow :3";
$temp = new DateTime(
datetime: $target,
);
return $temp->format("l d F Y");
}
public function PrettyTime(?string $target): string
{
if($target == [] || $target == "" || $target == null) return "meow :3";
$temp = new DateTime(
datetime: $target,
);
return $temp->format("h:i:s A");
}
public function PrettyDateTime(?string $target): string
{
return $this->PrettyDate(target: $target) . " at " . $this->PrettyTime(target: $target);
}
}

130
App/TwigExtensions/FunctionExtensions.php

@ -0,0 +1,130 @@
<?php
namespace App\TwigExtensions;
use App\Configuration;
use App\Wrappers\APIWrapper;
use App\Wrappers\JWTWrapper;
use App\Wrappers\LFSWrapper;
use Ramsey\Uuid\Uuid;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class FunctionExtensions extends AbstractExtension
{
private $twig;
public function __construct(Environment $twig)
{
$this->twig = $twig;
}
public function getFunctions()
{
return [
new TwigFunction('RandomUUID', [$this, 'RandomUUID'], ['is_safe' => ['html']]),
new TwigFunction('BuildForms', [$this, 'BuildForms'], ['is_safe' => ['html']]),
new TwigFunction('BuildModifiableField', [$this, 'BuildModifiableField'], ['is_safe' => ['html']]),
new TwigFunction('BuildEnumField', [$this, 'BuildEnumField'], ['is_safe' => ['html']]),
new TwigFunction('GetLFSFileURL', [$this, 'GetLFSFileURL'], ['is_safe' => ['html']]),
new TwigFunction('GetConfigurationItem', [$this, 'GetConfigurationItem'], ['is_safe' => ['html']]),
new TwigFunction('ParseForUserToken', [$this, 'ParseForUserToken'], ['is_safe' => ['html']]),
];
}
public function RandomUUID(): string
{
return Uuid::uuid4()->toString();
}
public function BuildForms(...$formDefinitions): string
{
$output = '<div>';
foreach ($formDefinitions as $formDefinition)
{
$output .= twig_include($this->twig, [], '/Partials/Form.html.twig', ['FormDefinition' => $formDefinition]);
}
return "$output</div>";
}
public function BuildModifiableField(string $apiBase, string $id, string $value): string
{
$output = twig_include($this->twig, [], '/Partials/ModifiableField.html.twig', ['FieldData' => [
"APIBase"=>$apiBase,
"ID"=>$id,
"Value"=>$value,
]]);
return "$output";
}
public function BuildEnumField(string $apiBase, string $id, string $value, string $enumName): string
{
$enumData = APIWrapper::Get("/Enumerators/$enumName")->Payload["Cases"];
foreach($enumData as $case)
{
if($case["Value"] == $value)
{
$currentEnum = $case;
}
}
$output = twig_include($this->twig, [], '/Partials/EnumField.html.twig', ['FieldData' => [
"APIBase"=>$apiBase,
"ID"=>$id,
"Value"=>$value,
"EnumName"=>$enumName,
"CurrentEnum"=>$currentEnum,
"EnumData"=>$enumData,
]]);
return "$output";
}
public function GetConfigurationItem(string...$nav): string
{
return Configuration::GetConfig(...$nav);
}
public function GetLFSFileURL(string $fileID): string
{
return LFSWrapper::GetFileURLByID($fileID);
}
public function ParseForUserToken(string $input) : string
{
$modes = implode(separator: "|", array:[
"USER",
"VEHICLE",
]);
$uuidRegex = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}";
$fullRegex = "/@({$modes}).{$uuidRegex}.{$uuidRegex}/";
preg_match_all($fullRegex, $input, $matches);
foreach($matches[0] as $match)
{
$parts = explode(".", $match);
$mode = substr(string: $parts[0], offset: 1);
$tenantID = $parts[1];
$objectID = $parts[2];
switch ($mode)
{
case "USER":
$temp = twig_include($this->twig, [], '/Partials/UserButton.html.twig', [
'_tenantID' => $tenantID,
'_userID' => $objectID,
]);
break;
case "VEHICLE":
break;
}
$input = str_replace($match, $temp, $input);
}
return $input;
}
}

45
App/TwigExtensions/NavigationExtension.php

@ -0,0 +1,45 @@
<?php
namespace App\TwigExtensions;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
require_once dirname($_SERVER["DOCUMENT_ROOT"]) . "/vendor/autoload.php";
class NavigationExtension extends AbstractExtension
{
public function getFunctions()
{
return [
new TwigFunction('RenderServiceLinks', [$this, 'RenderServiceLinks']),
new TwigFunction('RenderBackButton', [$this, 'RenderBackButton']),
];
}
public function RenderServiceLinks(): string
{
$serviceLinkHTML = "";
return "yrdy";
}
public function RenderBackButton(string $title) : string
{
$html = '
<div style="display: flex; align-items: center;">
<div style="cursor: pointer; display: flex; align-items: center;" onclick="window.history.back();">
<i class="material-icons">arrow_back</i>
<span>Back</span>
</div>
<h1 style="margin-left: 1em;">{{TITLE}}</h1>
<div style="display: table; clear: both"></div>
</div>
';
return str_replace(
search: "{{TITLE}}",
replace: $title,
subject: $html
);
}
}

36
App/TwigExtensions/StringManipulationExtension.php

@ -0,0 +1,36 @@
<?php
namespace App\TwigExtensions;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class StringManipulationExtension extends AbstractExtension
{
public function getFunctions()
{
return [
new TwigFunction('turn_mot_code_to_url', [$this, 'TurnMOTCodeToURL']),
];
}
public function TurnMOTCodeToURL(string $motCode) : string
{
$groups = [
7=>"7-other-equipment"
];
$motCodeElements = explode(separator: '.', string: $motCode);
$builder = "https://www.gov.uk/guidance/mot-inspection-manual-for-private-passenger-and-light-commercial-vehicles/";
$builder .= $groups[intval(value: $motCodeElements[0])];
$builder .= "#section-";
$builder .= "-";
$builder .= $motCodeElements[1];
$builder .= "-";
$builder .= $motCodeElements[2];
return $builder;
}
}

33
App/TwigTests/TwigTests.php

@ -0,0 +1,33 @@
<?php
// src/Twig/AppExtension.php
namespace App\TwigTests;
use Twig\Extension\AbstractExtension;
use Twig\TwigTest;
class TwigTests extends AbstractExtension
{
public function getTests()
{
return array(
new TwigTest('object', [$this, 'isObject']),
new TwigTest('array', [$this, 'isArray']),
new TwigTest('numeric', [$this, 'isNumeric']),
);
}
public function isObject($object)
{
return is_object($object);
}
public function isArray($value)
{
return is_array($value);
}
public function isNumeric($value)
{
return is_numeric($value);
}
}

18
App/Wrappers/AlgoliaInteractions.php

@ -0,0 +1,18 @@
<?php
namespace App\Wrappers;
use Algolia\AlgoliaSearch\Api\SearchClient;
use App\Configuration;
class AlgoliaInteractions
{
private SearchClient $SearchClient;
public function __construct()
{
$this->SearchClient = SearchClient::create(
appId: Configuration::GetConfig("Algolia", "AppID"),
apiKey: Configuration::GetConfig("Algolia", "Keys", "SearchOnly"),
);
}
}

77
App/Wrappers/DatabaseInteractions.php

@ -0,0 +1,77 @@
<?php
namespace App\Wrappers;
use App\API\Models\_DatabaseRecordPermissionSet;
use App\API\Models\EnumeratorDetails;
use App\API\Models\UserDetails;
use App\Configuration;
use Aura\SqlQuery\Common\DeleteInterface;
use Aura\SqlQuery\Common\InsertInterface;
use Aura\SqlQuery\Common\SelectInterface;
use Aura\SqlQuery\Common\UpdateInterface;
use Aura\SqlQuery\QueryFactory;
use PDO;
class DatabaseInteractions
{
private PDO $pdo;
public function __construct()
{
$servername = Configuration::GetConfig('MariaDB', 'Host');
$database = Configuration::GetConfig('MariaDB', 'Database');
$username = Configuration::GetConfig('MariaDB', 'Username');
$password = Configuration::GetConfig('MariaDB', 'Password');
$this->pdo = new PDO(
"mysql:host=$servername;dbname=$database", $username, $password
);
}
public function ScheduleRowForDeletion(string $tableName, string $id)
{
}
public function RunSelect(SelectInterface $queryBuilder): array
{
$sth = $this->pdo->prepare($queryBuilder->getStatement());
$sth->execute($queryBuilder->getBindValues());
$result = $sth->fetchAll(PDO::FETCH_ASSOC);
return $result;
}
public function RunOneSelect(SelectInterface $queryBuilder): array
{
$sth = $this->pdo->prepare($queryBuilder->getStatement());
$sth->execute($queryBuilder->getBindValues());
$result = $sth->fetchAll(PDO::FETCH_ASSOC);
if(sizeof($result) == 1) return $result[0];
echo "invalid single row db response";
die();
}
public function UpdateSingleField(UpdateInterface $queryBuilder): void
{
$sth = $this->pdo->prepare($queryBuilder->getStatement());
$sth->execute($queryBuilder->getBindValues());
$result = $sth->fetchAll(PDO::FETCH_ASSOC);
}
public function RunInsert(InsertInterface $queryBuilder): bool
{
$sth = $this->pdo->prepare($queryBuilder->getStatement());
return $sth->execute($queryBuilder->getBindValues());
}
public function RunDelete(DeleteInterface $queryBuilder): bool
{
$sth = $this->pdo->prepare($queryBuilder->getStatement());
return $sth->execute($queryBuilder->getBindValues());
}
}

41
App/Wrappers/SQLQueryBuilderWrapper.php

@ -0,0 +1,41 @@
<?php
namespace App\Wrappers;
use App\Common\DateTimeHandling;
use App\Common\ErrorPayloadRendering;
use Aura\SqlQuery\Common\DeleteInterface;
use Aura\SqlQuery\QueryFactory;
use DateTime;
class SQLQueryBuilderWrapper
{
public static function SELECT(string $table)
{
$query_factory = new QueryFactory(db: 'mysql');
$query = $query_factory->newSelect()
->from("$table AS T")
;
return $query
->cols([
"T.*",
]);
}
public static function SELECT_ONE(string $table, string $id)
{
return self::SELECT(table: $table)
->where(cond: "ID=:__id__")
->bindValue(name: "__id__", value: $id)
->limit(limit: 1)
;
}
public static function SELECT_WITH_PARENT_ID(string $table, string $parentIDName, string $parentIDValue)
{
return self::SELECT(table: $table)
->where(cond: "$parentIDName=:__parent_id__")
->bindValue(name: "__parent_id__", value: $parentIDValue)
;
}
}

15
Configuration/Configuration.yaml

@ -0,0 +1,15 @@
Algolia:
AppID: ;ALGOLIA__APP_ID
IndexName: FolkTunes_LIVE
APIKeys:
SearchOnly: ;ALGOLIA__API_SEARCH_KEY
Write: ;ALGOLIA__API_WRITE_KEY
Admin: ;ALGOLIA__API_ADMIN_KEY
MariaDB:
Host: vps-gen-eng-001.infra.scorpia.network
Port: 3306
Username: ;MARIADB__USERNAME
Password: ;MARIADB__PASSWORD
Database: FolkTuneFinder_Live

0
LocalStorage/.gitkeep

0
LocalStorage/Tunes/.gitkeep

160
Localisation/en-GB.yaml

@ -0,0 +1,160 @@
##################################################
# A
##################################################
##################################################
# B
##################################################
##################################################
# C
##################################################
##################################################
# D
##################################################
##################################################
# E
##################################################
##################################################
# F
##################################################
##################################################
# G
##################################################
##################################################
# H
##################################################
##################################################
# I
##################################################
##################################################
# J
##################################################
##################################################
# K
##################################################
##################################################
# L
##################################################
##################################################
# M
##################################################
##################################################
# N
##################################################
##################################################
# O
##################################################
##################################################
# P
##################################################
##################################################
# Q
##################################################
##################################################
# R
##################################################
##################################################
# S
##################################################
##################################################
# T
##################################################
##################################################
# U
##################################################
##################################################
# V
##################################################
##################################################
# W
##################################################
##################################################
# X
##################################################
##################################################
# Y
##################################################
##################################################
# Z
##################################################
##################################################
# _
##################################################

8
Pages/index.php

@ -0,0 +1,8 @@
<?php
use App\Wrappers\TwigWrapper;
require_once __DIR__ . "/../vendor/autoload.php";
TwigWrapper::AutoRenderTwig([
]);

22
Pages/tune/uuid.php

@ -0,0 +1,22 @@
<?php
use App\Wrappers\DatabaseInteractions;
use App\Wrappers\SQLQueryBuilderWrapper;
use App\Wrappers\TwigWrapper;
$db = new DatabaseInteractions();
$tuneDetails = $db->RunOneSelect(
queryBuilder: SQLQueryBuilderWrapper::SELECT_ONE(
table: 'Tunes',
id: $_GET['tune-id']
),
);
TwigWrapper::RenderTwig(
target: "Pages/tune/uuid.html.twig",
arguments: [
"TuneDetails"=>$tuneDetails,
]
);

37
Public/Static/CSS/Elements/HomePage.css

@ -0,0 +1,37 @@
.InnerContent {
margin: 0 auto;
text-align: center;
}
input[type="text"] {
width: 80%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
#AlgoliaResults {
margin-top: 20px;
text-align: left;
}
.AlgoliaTuneHit {
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #fff;
margin-bottom: 10px;
}
.AlgoliaTuneHit h2 {
}
.AlgoliaTuneHit .TuneTimeSignature {
}
.AlgoliaTuneHit .TuneKeySignature {
}

21
Public/Static/CSS/Elements/_General.css

@ -0,0 +1,21 @@
* {
margin: 0;
padding: 0;
}
body{
font-family: 'Atkinson Hyperlegible', sans-serif;
font-style: normal;
font-weight: 300;
font-smoothing: antialiased;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 1rem;
}
main {
background: var(--colour--main--background);
}

11
Public/Static/CSS/Mapper.css

@ -0,0 +1,11 @@
/* FONTS */
@import url('https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible&display=swap');
/* ICONS */
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css');
/* ELEMENTS */
@import "/Static/CSS/Elements/_General.css";
@import "/Static/CSS/Elements/HomePage.css";

2
Public/Static/CSS/Themes/Light.css

@ -0,0 +1,2 @@
:root {
}

30
Public/Static/JS/AlgoliaInteractions.js

@ -0,0 +1,30 @@
function searchTunes(query) {
const resultsDiv = document.getElementById("AlgoliaResults");
resultsDiv.innerHTML = "";
index.search(query).then(({ hits }) => {
if (hits.length > 0) {
hits.forEach(hit => {
const tuneDiv = document.createElement("div");
tuneDiv.className = "AlgoliaTuneHit";
tuneDiv.innerHTML = `
<h2><a href="/tune/${hit.objectID}">${hit.title}</a></h2>
<div class="TuneTimeSignature">time signature: ${hit.time_sig}</div>
<div class="TuneKeySignature">key signature: ${hit.key_sig}</div>
`;
resultsDiv.appendChild(tuneDiv);
});
} else {
resultsDiv.innerHTML = "<p>No tunes found. Try a different search!</p>";
}
}).catch(err => {
console.error('Error searching Algolia:', err);
resultsDiv.innerHTML = "<p>An error occurred. Please try again later.</p>";
});
}

BIN
Public/favicon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

29
Routing/Router.php

@ -0,0 +1,29 @@
<?php
use App\Configuration;
use App\Enumerators\SessionElement;
use App\Wrappers\TwigWrapper;
require_once __DIR__ . "/../vendor/autoload.php";
session_start();
// Get the request URI and break it into segments
$requestUri = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$requestElements = explode("/", trim($requestUri, "/"));
switch($requestElements[0])
{
case "Static":
case "favicon.ico":
return false;
case "":
require_once __DIR__ . '/../Pages/index.php';
return true;
case "tune":
$_GET['tune-id'] = $requestElements[1];
require_once __DIR__ . '/../Pages/tune/uuid.php';
return true;
}
return false;

26
Templates/Bases/ErrorPage.html.twig

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="shortcut icon" type="image/png" href="/favicon.png?type=png" />
<title>{{ "Folk Tunes"|translate }} | {% block PageTitle %}{% endblock %}</title>
<link rel="stylesheet" href="/Static/CSS/Mapper.css">
<link rel="stylesheet" href="/Static/CSS/Themes/Light.css">
</head>
<body>
<main>
<div class="ErrorPageContainer">
<h1>{% block error_code %}{% endblock %}</h1>
<h2>{% block error_message %}{% endblock %}</h2>
<a class="Button" href="/">Back to Home</a>
</div>
</main>
</body>
</html>

33
Templates/Bases/StandardWebPage.html.twig

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="shortcut icon" type="image/png" href="/favicon.png?type=png" />
<title>{{ "Folk Tunes"|translate }} | {% block PageTitle %}{% endblock %}</title>
<link rel="stylesheet" href="/Static/CSS/Mapper.css">
<link rel="stylesheet" href="/Static/CSS/Themes/Light.css">
<script src="https://cdn.jsdelivr.net/npm/algoliasearch/dist/algoliasearch-lite.umd.js"></script>
<script>
const client = algoliasearch('{{ _ALGOLIA_APP_ID_ }}', '{{ _ALGOLIA_SEARCH_ONLY_API_KEY_ }}');
const index = client.initIndex('{{ _ALGOLIA_INDEX_NAME_ }}');
</script>
<script src="/Static/JS/AlgoliaInteractions.js"></script>
</head>
<body>
<div class="wrapper">
<div class="main_container">
<main>
{% block content %}{% endblock %}
</main>
</div>
</div>
</body>
</html>

4
Templates/ErrorPages/404.html.twig

@ -0,0 +1,4 @@
{% extends "/Bases/ErrorPage.html.twig" %}
{% block error_code %}404{% endblock %}
{% block error_message %}{{ "Page Not Found"|translate }}{% endblock %}

21
Templates/Pages/index.html.twig

@ -0,0 +1,21 @@
{% extends "/Bases/StandardWebPage.html.twig" %}
{% block content %}
<div class="InnerContent">
<h1>Folk Tune Search</h1>
<input type="text" id="SearchInput" placeholder="Search for a tune...">
<div id="AlgoliaResults"></div>
</div>
<script>
// Event listener for real-time search
document.getElementById("SearchInput").addEventListener("input", (event) => {
const query = event.target.value;
if (query.trim().length > 0) {
searchTunes(query);
} else {
document.getElementById("AlgoliaResults").innerHTML = "";
}
});
</script>
{% endblock %}

7
Templates/Pages/tune/uuid.html.twig

@ -0,0 +1,7 @@
{% extends "/Bases/StandardWebPage.html.twig" %}
{% block content %}
<div class="InnerContent">
<h1>{{ TuneDetails.Title }}</h1>
</div>
{% endblock %}

32
composer.json

@ -0,0 +1,32 @@
{
"name": "darksparrow/folktunes",
"description": "Folk Tune Search Engine",
"type": "project",
"license": "proprietary",
"authors": [
{
"name": "Cerys Lewis",
"email": "cerys@darksparrow.uk",
"role": "Lead Developer"
}
],
"support": {
"email": "systems@darksparrow.uk"
},
"autoload": {
"psr-4": {
"App\\": "App/"
}
},
"require": {
"php": ">=8.2",
"zircote/swagger-php": "~4.7.9",
"twig/twig": "3.14.1.0",
"algolia/algoliasearch-client-php": "*",
"ext-pdo": "*",
"aura/sqlquery": "2.8.1"
}
}

1326
composer.lock

File diff suppressed because it is too large
Loading…
Cancel
Save