Initial commit.
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 31s

This commit is contained in:
Cameron Redmore 2025-04-18 16:28:19 +01:00
commit 19da15822c
192 changed files with 14974 additions and 0 deletions

9
www/css/custom.css Normal file
View file

@ -0,0 +1,9 @@
* {
box-sizing: border-box;
font-family: 'Chakra Petch', sans-serif;
}
html, body {
min-height: 100% !important;
}

3
www/css/input.css Normal file
View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

608
www/demos/filters.html Normal file
View file

@ -0,0 +1,608 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Search Example</title>
<style>
html, body {
background: var(--background-1);
color: white;
}
:root {
--background-1: #202020;
--background-2: #2f2f3f;
--accent: #FF9F1C;
}
* {
box-sizing: border-box;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
color-scheme: dark light;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.search-flex {
display: flex;
justify-content: space-between;
flex-direction: row;
flex-wrap: wrap;
min-height: 750px;
border-radius: 25px;
overflow: hidden;
border: 1px solid #2f2f3f;
}
/* Sidebar should be 25% width, but break to own row at < 200px */
.sidebar {
background-color: var(--background-2);
/* 25% width */
flex: 1 0 25%;
/* Break to own row at < 200px */
min-width: 200px;
border-right: 1px solid #2f2f3f;
}
.main {
/* 75% width */
flex: 1 1 75%;
/* Fill height */
flex-grow: 1;
display: flex;
flex-direction: column;
}
.filters {
background: var(--background-2);
/* Minimum height of 100px */
flex: 0 1 100px;
border-bottom: 1px solid #2f2f3f;
display: flex;
align-items: center;
flex-direction: row;
gap: 10px;
padding: 10px;
}
.filters>* {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: 0.25s;
padding: 10px;
background: rgba(255, 255, 255, 0.2);
height: 30px;
text-transform: uppercase;
border-radius: 25px;
}
.filters>*:hover {
background: rgba(255, 255, 255, 0.25);
}
.results {
background: #028090;
/* Fill rest of space */
flex: 0 1 75svh;
/* Scroll if needed */
overflow-y: auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
.currentFilter {
overflow: hidden;
height: auto;
flex-basis: 0;
flex-shrink: 1;
transition: 0.5s;
display: flex;
flex-direction: column;
}
.currentFilter.active {
flex-basis: 200px;
}
.filterValues {
flex: 1 1 auto;
/* Set as Flex, try to use 3 columns, but break to more rows if less than 200px per item */
display: flex;
flex-wrap: wrap;
justify-content: space-around;
overflow-y: auto;
gap: 10px;
padding: 10px;
}
.filterValues>div {
/* 33% width, but break to own row at < 200px */
flex: 1 0 calc((100% /3) - 10px);
min-width: 200px;
padding: 5px;
display: flex;
background: #02C39A;
padding: 5px;
border-radius: 5px;
justify-content: center;
align-items: center;
cursor: pointer;
transition: 0.25s;
}
.filterValues>div:hover {
background: #00ecb9;
}
.showAllButton {
bottom: 0;
flex: 0 0 25px;
text-align: center;
background: rgba(255, 255, 255, 0.25);
cursor: pointer;
transition: 0.25s;
}
.showAllButton:hover {
background: rgba(255, 255, 255, 0.5);
}
.selectedFilters {
overflow: hidden;
border-bottom: 1px solid #2f2f3f;
display: flex;
flex-direction: row;
flex-wrap: wrap;
transition: 0.5s;
flex-basis: min-content;
}
.selectedFilters .chip {
background: var(--accent);
padding: 10px;
margin: 10px;
border-radius: 25px;
display: flex;
align-items: center;
justify-content: center;
text-transform: uppercase;
font-weight: bold;
cursor: pointer;
scale: 0;
animation: fadeIn 0.5s forwards;
}
.selectedFilters .chip.fadeOut {
animation: fadeOut 0.5s forwards;
}
@keyframes fadeIn {
from {
scale: 0;
}
to {
scale: 1;
}
}
@keyframes fadeOut {
from {
scale: 1;
}
to {
scale: 0;
}
}
.filler {
flex: 1 1 auto;
background: transparent;
text-align: center;
text-transform: uppercase;
pointer-events: none;
}
.sort {}
.pagination {
flex: 0 1 25%;
display: flex;
flex-direction: row;
gap: 10px;
justify-content: space-around;
}
.product {
background: #00A896;
padding: 10px;
margin: 10px;
border-radius: 10px;
display: flex;
flex-direction: column;
text-align: center;
}
</style>
</head>
<body>
<h1>Search Filter Example</h1>
<p>
Please note, this is still *very* early. It is a proof of concept, not attached to any real data, and is designed to allow experimentation with a *potential* filtering method.
</p>
<p>
This example is still missing dynamic filters entirely, and sort and pagination are yet to be implemented. It is also not yet wholly responsive.
Additionally, the counts on the category and brand buttons are not yet implemented, and filter chips are not sorted in any way.
</p>
<div class="container">
<!-- Layout with sidebar, static filters & sort, then main table -->
<div class="search-flex">
<div class="sidebar">
<h1 style="text-align: center; margin: auto;">Dynamic Filters Go Here</h1>
</div>
<div class="main">
<div class="filters">
<div class="category">Category (X)</div>
<div class="brand">Brand (X)</div>
<div class="filler"></div>
<div class="sort">Sort</div>
<div class="pagination">
<div>Previous</div>
<div>Next</div>
</div>
</div>
<div class="currentFilter">
<div class="filterValues">
</div>
<span class="showAllButton">Show All</span>
</div>
<div class="selectedFilters">
<!-- <div class="chip">Filter 1</div> -->
</div>
<div class="results">
</div>
</div>
</div>
</div>
<script>
const products = [];
const selectedFilters = {
category: [],
brand: []
};
const selectedDynamicFilters = [];
const filters = document.querySelector('.currentFilter');
const showAllButton = document.querySelector('.showAllButton');
showAllButton.addEventListener('click', () => {
const hasSize = filters.style.flexBasis;
if (hasSize) {
filters.style.flexBasis = '';
} else {
filters.style.flexBasis = (document.querySelector('.filterValues').scrollHeight + 25) + 'px';
}
});
const category = document.querySelector('.category');
const brand = document.querySelector('.brand');
let currentFilters = null;
const filterValues = document.querySelector('.filterValues');
const filterOptions = {
category: ['Category 1', 'Category 2', 'Category 3', 'Category 4', 'Category 5', 'Category 6', 'Category 7', 'Category 8', 'Category 9', 'Category 10', 'Category 11', 'Category 12', 'Category 13', 'Category 14', 'Category 15', 'Category 16', 'Category 17', 'Category 18', 'Category 19', 'Category 20'],
brand: ['Brand 1', 'Brand 2', 'Brand 3', 'Brand 4', 'Brand 5', 'Brand 6', 'Brand 7', 'Brand 8', 'Brand 9', 'Brand 10', 'Brand 11', 'Brand 12', 'Brand 13', 'Brand 14', 'Brand 15', 'Brand 16', 'Brand 17', 'Brand 18', 'Brand 19', 'Brand 20']
}
const dynamicFilters = {
size: ['50mm', '100mm', '150mm', '200mm'],
color: ['Red', 'Green', 'Blue', 'Yellow', 'Orange'],
use: ['Indoor', 'Outdoor', 'Industrial', 'Commercial', 'Residential']
}
for (let i = 0; i < 1000; i++) {
const newProd = {
id: i,
name: `Product ${i}`,
category: filterOptions.category[Math.floor(Math.random() * filterOptions.category.length)],
brand: filterOptions.brand[Math.floor(Math.random() * filterOptions.brand.length)],
attributes: {
},
price: (Math.floor(Math.random() * 100000) / 100).toFixed(2)
};
if (Math.random() > 0.5) {
newProd.attributes.size = dynamicFilters.size[Math.floor(Math.random() * dynamicFilters.size.length)];
}
if (Math.random() > 0.66) {
newProd.attributes.color = dynamicFilters.color[Math.floor(Math.random() * dynamicFilters.color.length)];
}
if (Math.random() > 0.75) {
newProd.attributes.use = dynamicFilters.use[Math.floor(Math.random() * dynamicFilters.use.length)];
}
products.push(newProd);
}
async function openOrCloseFilters(type) {
if (currentFilters === type) {
currentFilters = null;
filters.classList.remove('active');
filters.style.flexBasis = '';
return;
}
else if (!currentFilters) {
currentFilters = type;
filters.classList.add('active');
}
else {
currentFilters = type;
//Hide current filters, switch to new filters
filters.classList.remove('active');
filters.style.flexBasis = '0px';
await new Promise((resolve, reject) => {
setTimeout(() => {
filters.classList.add('active');
filters.style.flexBasis = '';
resolve();
}, 500);
});
}
filterValues.innerHTML = '';
filterOptions[type].forEach((option) => {
const div = document.createElement('div');
div.innerText = option;
//Calculate how many products remaining if this filter is applied
const filteredProducts = getFilteredProducts().filter((product) => {
return product[type] === option;
});
if (filteredProducts.length > 0) {
div.innerText += ` (${filteredProducts.length})`;
div.addEventListener('click', () => {
addOrRemoveFilter(type, option);
});
filterValues.appendChild(div);
}
});
}
function addOrRemoveFilter(filterType, filterValue) {
console.log('Adding or removing filter', filterType, filterValue)
console.log(selectedFilters);
if (selectedFilters[filterType].includes(filterValue)) {
removeFilter(filterType, filterValue);
}
else {
addFilter(filterType, filterValue);
}
}
function addFilter(filterType, filterValue) {
selectedFilters[filterType].push(filterValue);
const chip = document.createElement('div');
chip.classList.add('chip');
chip.setAttribute('data-filter-type', filterType);
chip.setAttribute('data-filter-value', filterValue);
chip.innerText = filterValue;
chip.addEventListener('click', () => {
chip.classList.add('fadeOut');
setTimeout(() => {
chip.remove();
}, 500);
removeFilter(filterType, filterValue);
});
document.querySelector('.selectedFilters').appendChild(chip);
updateProductsTable();
}
function removeFilter(filterType, filterValue) {
if (selectedFilters[filterType].includes(filterValue)) {
selectedFilters[filterType] = selectedFilters[filterType].filter((value) => value !== filterValue);
}
//Check for chip
const chip = document.querySelector(`.selectedFilters .chip[data-filter-type="${filterType}"][data-filter-value="${filterValue}"]`);
if (chip) {
chip.classList.add('fadeOut');
setTimeout(() => {
chip.remove();
}, 500);
}
updateProductsTable();
}
function getFilteredProducts() {
let filteredProducts = products;
for (const filterType in selectedFilters) {
if (selectedFilters.hasOwnProperty(filterType)) {
const filterValues = selectedFilters[filterType];
if (filterValues.length === 0) {
continue;
}
filteredProducts = filteredProducts.filter((product) => {
return filterValues.includes(product[filterType]);
});
}
}
//Dynamic filters
for (const filterType in selectedDynamicFilters) {
if (selectedDynamicFilters.hasOwnProperty(filterType)) {
const filterValues = selectedDynamicFilters[filterType];
if (filterValues.length === 0) {
continue;
}
filteredProducts = filteredProducts.filter((product) => {
return filterValues.includes(product.attributes[filterType]);
});
}
}
return filteredProducts;
}
function updateProductsTable() {
console.log('Updating products table');
const results = document.querySelector('.results');
results.innerHTML = '';
const filteredProducts = getFilteredProducts();
filteredProducts.forEach((product) => {
//Create divs for each product attribute
const div = document.createElement('div');
div.classList.add('product');
div.innerHTML = `
<h3>${product.name}</h3>
<p>${product.category}</p>
<p>${product.brand}</p>
<p>Size: ${product.attributes.size || 'N/A'}</p>
<p>Colour: ${product.attributes.color || 'N/A'}</p>
<p>Use: ${product.attributes.use || 'N/A'}</p>
<p>Price: $${product.price}</p>
`;
results.appendChild(div);
});
//Update filler with product count
document.querySelector('.filler').innerText = `Showing ${filteredProducts.length}/${products.length} products`;
}
category.addEventListener('click', () => {
openOrCloseFilters('category');
});
brand.addEventListener('click', () => {
openOrCloseFilters('brand');
});
updateProductsTable();
</script>
</body>
</html>

BIN
www/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

142
www/index.html Normal file
View file

@ -0,0 +1,142 @@
<!DOCTYPE html>
<html lang="en" class="bg-gradient-to-br from-gray-100 to-gray-300 dark:from-gray-900 dark:to-gray-700 dark:text-white">
<head>
<title>Cameron's Corner</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="UTF-8" />
<meta name="description" content="Cameron's Corner, my own little slice of the internet! By: Cameron Redmore">
<link rel="preload" as="style" onload="this.onload=null;this.rel='stylesheet'" type="text/css" href="https://fonts.googleapis.com/css2?family=Chakra+Petch&display=swap">
<link rel="stylesheet" type="text/css" href="css/style.css" defer>
<link rel="stylesheet" type="text/css" href="css/custom.css" defer>
</head>
<body class="text-center">
<h1 class="text-3xl font-bold rainbow-text">Cameron's Corner</h1>
<hr class="border-gray-500"/>
<p class="text-xl my-2 px-4">Welcome to Cameron's Corner, my own little slice of the Internet.</p>
<!-- Projects -->
<hr class="border-gray-500"/>
<h2 class="text-2xl font-bold underline mt-2">Current Projects</h2>
<p class="text-lg my-2 px-4">Looking for things I've done? Check out some of my projects!</p>
<div class="container mx-auto flex flex-wrap justify-evenly gap-4 my-4">
<!-- CompuVerse -->
<!-- <a href="https://compuverse.uk/" target="_blank" class="w-fill border border-gray-500 w-64 h-64 flex flex-col justify-around rounded-lg text-blue-400">
<span class="iconify w-16 h-16 mx-auto text-blue-500" data-icon="ri:computer-line" data-inline="false"></span>
<hr class="border-gray-500"/>
CompuVerse
<p class="text-black dark:text-white">CompuVerse is my Lemmy instance, all about technology!</p>
</a> -->
<!-- SlideWords -->
<a href="https://slidewords.net/" target="_blank" class="w-fill border border-gray-500 w-64 h-64 flex flex-col justify-around rounded-lg text-blue-400">
<span class="iconify w-16 h-16 mx-auto text-blue-500" data-icon="material-symbols-light:format-letter-spacing-2-rounded" data-inline="false"></span>
<hr class="border-gray-500"/>
SlideWords
<p class="text-black dark:text-white">SlideWords is a little daily game about sliding letters to form words, easy to pick up, impossible to master!</p>
</a>
<!-- The Freedom Frontier -->
<!-- <a href="https://frontier.cmzi.uk/" target="_blank" class="w-fill border border-gray-500 w-64 h-64 flex flex-col justify-around rounded-lg text-blue-400">
<span class="iconify w-16 h-16 mx-auto text-green-500" data-icon="mdi-minecraft" data-inline="false"></span>
<hr class="border-gray-500"/>
The Freedom Frontier
<p class="text-black dark:text-white">The Freedom Frontier is my Minecraft All-Op server!</p>
</a> -->
<a href="https://yams.cmzi.uk/" target="_blank" class="w-fill border border-gray-500 w-64 h-64 flex flex-col justify-around rounded-lg text-blue-400">
<span class="iconify w-16 h-16 mx-auto text-orange-500" data-icon="mdi:puzzle" data-inline="false"></span>
<hr class="border-gray-500"/>
Ỵ̷͍̜͍̹̠̺͍̅́͋́̔̿̎̔͐͑̿̊͘A̶̢͕̺̤̼͈͍̦͍̿̍̿̒͠M̷̢͍̘̣͉̤̥̬̓̀͒̏͒̓̐̄͐̃̈́́̃̀̚S̸̡͕̦̯͕̻̖̭̥͍̬̍̿͆̾̾͛͑̓̇͜͝͠ͅ
<p class="text-black dark:text-white">Y̷̪̻̟̪̺̟̠͓͚̥̠͒A̴͚͖͕̯̳͍̰̣̩̭̭̼̦͋̐͐͝M̶̞̟̝̹͔͔̯̳̟͂̀̓̓̅͊͘͠ͅS̴̢̛̯̣͈͚̼̞͍̞̯̩͙͌̔ ̸̖̮̹̪͖͍̟̥̥̼̩̯̹̜͂̓͛̋̃͒̓̍̋͆͂̏̕͝͝Y̶̡̡̛̙̠̜̪̫͙͈̞̝̪̜͋̔́̑́̕͝A̸̛͇̜̋͒̾̽̊̿̿̾̍́̒͆M̶̢̡̰͎̼̠̟͙̘̜̐̏͑̿͋̇̔̀͂͒͋S̴̢̧̨̮̺̲͖͕̺͙͉̯̯͍̏̄ͅ ̴̢̯̻̫͎̩̬̊̾͋͐͑͌̎͆͊̔͐̈̔͌͝ͅͅY̸̢̡̧̖̼̣̺͎̠͔̻͍̹͙͑̄́̀͛̋ͅA̵͉̜̞̰̮̬͇̝̙͇͇͒Ṁ̷̛̖͒́̉̽̉̒̀̑̄̌͑̕̚S̸̠̘͉̀̍ ̷̟̆̏̋̓̿͛̊̈̓͋̊Y̶̦̏́͆̅͌̋A̸̡̡̙̮̼͖̬̬̳̦̅͂͌̽̌̄̋͌͂͗́̚̚̕͝Ḿ̶̡̬̺̪̖̼͈̳͎͖̯̱̐̽̌͋̚͜S̵̡̛̳̻̠͎̫̒́ ̷̡̧̨̞͎̩̣̝̘̻̱̟̽͌͒͋̿͜͝͠Y̵̜̼͍̎̇͒̆͋͑̽̒͗͗͌͘Ą̵̧̛͔̞̦͖̩̞̥͕͈͔̟͗̓͋̽̎̌̔̉̕͜ͅM̷̥̳̼͕̟̓̀̽͒̀͝͠S̵͎̺̱̞̍̐̆́̆̈́͝</p>
</a>
<!-- Cameron's Cloud Community -->
<a href="https://discord.gg/UFSuy5zhNP" target="_blank" class="w-fill border border-gray-500 w-64 h-64 flex flex-col justify-around rounded-lg text-blue-400">
<span class="iconify w-16 h-16 mx-auto text-indigo-500" data-icon="akar-icons:discord-fill" data-inline="false"></span>
<hr class="border-gray-500"/>
Cameron's Cloud Community
<p class="text-black dark:text-white px-4">CCC is a Discord server for fans of gaming.</p>
</a>
</div>
<!-- Socials -->
<hr class="border-gray-500"/>
<h2 class="text-2xl font-bold underline mt-2">Socials</h2>
<p class="text-lg my-2 px-4">Want to get in touch? You can find me at all of these places!</p>
<div class="container mx-auto flex flex-wrap justify-evenly gap-4 my-4 pb-8 px-1">
<!-- Twitter / X: CameronRedmore -->
<a href="https://twitter.com/CameronRedmore" target="_blank" class="w-fill border border-gray-500 w-40 h-40 flex flex-col justify-around rounded-lg text-blue-400">
<span class="iconify w-16 h-16 mx-auto text-black dark:text-white" data-icon="akar-icons:x-fill" data-inline="false"></span>
<hr class="border-gray-500"/>
@CameronRedmore
</a>
<!-- YouTube: Camzie99 -->
<a href="https://youtube.com/Camzie99" target="_blank" class="w-fill border border-gray-500 w-40 h-40 flex flex-col justify-around rounded-lg text-blue-400">
<span class="iconify w-16 h-16 mx-auto text-red-500" data-icon="akar-icons:youtube-fill" data-inline="false"></span>
<hr class="border-gray-500"/>
Camzie99
</a>
<!-- GitHub: CameronRedmore -->
<a href="https://github.com/CameronRedmore" target="_blank" class="w-fill border border-gray-500 w-40 h-40 flex flex-col justify-around rounded-lg text-blue-400">
<span class="iconify w-16 h-16 mx-auto text-black dark:text-white" data-icon="akar-icons:github-fill" data-inline="false"></span>
<hr class="border-gray-500"/>
CameronRedmore
</a>
<!-- Reddit: Camzie99 -->
<a href="https://reddit.com/user/Camzie99" target="_blank" class="w-fill border border-gray-500 w-40 h-40 flex flex-col justify-around rounded-lg text-blue-400">
<span class="iconify w-16 h-16 mx-auto text-orange-500" data-icon="akar-icons:reddit-fill" data-inline="false"></span>
<hr class="border-gray-500"/>
Camzie99
</a>
<!-- Twitch: Camzie99 -->
<a href="https://twitch.tv/Camzie99" target="_blank" class="w-fill border border-gray-500 w-40 h-40 flex flex-col justify-around rounded-lg text-blue-400">
<span class="iconify w-16 h-16 mx-auto text-purple-500" data-icon="akar-icons:twitch-fill" data-inline="false"></span>
<hr class="border-gray-500"/>
Camzie99
</a>
<!-- Steam: Camzie99 -->
<a href="https://steamcommunity.com/id/Camzie99" target="_blank" class="w-fill border border-gray-500 w-40 h-40 flex flex-col justify-around rounded-lg text-blue-400">
<span class="iconify w-16 h-16 mx-auto text-black dark:text-white" data-icon="ri:steam-fill" data-inline="false"></span>
<hr class="border-gray-500"/>
Camzie99
</a>
<!-- Discord: Camzie99#0001 -->
<a href="https://discord.com/users/147767470659338241" target="_blank" class="w-fill border border-gray-500 w-40 h-40 flex flex-col justify-around rounded-lg text-blue-400">
<span class="iconify w-16 h-16 mx-auto text-indigo-500" data-icon="akar-icons:discord-fill" data-inline="false"></span>
<hr class="border-gray-500"/>
Camzie99#0001
</a>
<!-- Email: ??? -->
<a href="" target="_blank" class="w-fill border border-gray-500 w-40 h-40 flex flex-col justify-around rounded-lg text-blue-400">
<span class="iconify w-16 h-16 mx-auto text-pink-500" data-icon="akar-icons:envelope" data-inline="false"></span>
<hr class="border-gray-500"/>
</a>
<!-- Matrix: @cameron:conduit.compuverse.uk -->
<!-- <a href="https://matrix.to/#/@cameron:conduit.compuverse.uk"
target="_blank" class="w-fill border border-gray-500 w-64 h-40 flex flex-col justify-around rounded-lg text-blue-400">
<span class="iconify w-16 h-16 mx-auto text-green-500" data-icon="tabler:brand-matrix" data-inline="false"></span>
<hr class="border-gray-500"/>
@cameron:conduit.compuverse.uk
</a> -->
</div>
<div class="fixed bottom-0 border-t border-gray-500 bg-gray-300 dark:bg-gray-700 w-full text-xl py-2">
Copyright &copy; Cameron Redmore <span id="currentYear">2023</span>
</div>
<script src="//code.iconify.design/1/1.0.6/iconify.min.js" defer></script>
<script src="js/obf.js">
</script>
<script>
document.querySelectorAll('a[href=""]').forEach((el) => {
el.href = "mailto:" + generateEmail();
el.innerHTML += generateEmail();
});
document.getElementById("currentYear").innerHTML = new Date().getFullYear();
</script>
</body>
</html>

0
www/js/main.js Normal file
View file

1
www/js/obf.js Normal file

File diff suppressed because one or more lines are too long

2
www/privacy.html Normal file
View file

@ -0,0 +1,2 @@
You login with a pre-determined username and password.<br/>
No data is collected.

BIN
www/res/discord.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB