Builds a website

This commit is contained in:
Alicia Sykes
2023-04-17 22:07:40 +01:00
parent 4d5dbf26c4
commit 2fb9a402eb
20 changed files with 3424 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

2715
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "portainer-templates",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.5.0",
"sass": "^1.62.0",
"svelte": "^3.54.0",
"svelte-check": "^3.0.1",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^4.2.0"
},
"type": "module"
}

12
src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};

12
src/app.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

41
src/lib/Categories.svelte Normal file
View File

@@ -0,0 +1,41 @@
<script lang="ts">
export let categories: string[];
export let selectedCategories: string[];
export let toggleCategory: (category: string) => void;
</script>
<div class="categories">
{#each Object.keys(categories) as category}
<span
on:click={() => toggleCategory(category)}
class:selected="{selectedCategories.includes(category)}"
class="cat"
>{category}</span>
{/each}
</div>
<style lang="scss">
.categories {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 1rem auto;
padding: 0 1rem;
max-width: var(--max-width);
.cat {
border: 1px solid transparent;
padding: 0 0.3rem;
margin: 0.25rem;
line-height: 2rem;
border-radius: 6px;
text-transform: capitalize;
background: var(--card);
transition: all 0.3s ease-in-out;
cursor: pointer;
font-size: 0.9rem;
&:hover, &.selected {
background: var(--gradient);
}
}
}
</style>

51
src/lib/Footer.svelte Normal file
View File

@@ -0,0 +1,51 @@
<script lang="ts">
import Icon from '$lib/Icon.svelte';
let footerInfo = {
author: 'Alicia Sykes',
authorSite: 'https://github.com/lissy93',
license: 'MIT',
licenseLink: 'https://gist.github.com/Lissy93/143d2ee01ccc5c052a17',
copyright: true,
source: 'https://github.com/lissy93/portainer-templates',
};
</script>
<footer>
<p>
© <a href={footerInfo.authorSite} target="_blank" rel="noreferrer">{footerInfo.author}</a>
{new Date().getFullYear()} - Licensed under
<a href={footerInfo.licenseLink} target="_blank" rel="noreferrer">{footerInfo.license}</a> -
View on <a href={footerInfo.source} target="_blank" rel="noreferrer">
GitHub <Icon name="github" color="var(--accent)" /></a>
</p>
</footer>
<style lang="scss">
footer {
bottom: 0;
padding: 0.5rem 0;
width: 100%;
background: var(--card);
p {
margin: 0;
text-align: center;
a {
color: var(--accent);
border-radius: 4px;
padding: 0.1rem 0.25rem;
text-decoration: none;
display: inline-flex;
flex-direction: revert;
gap: 0.25rem;
align-items: center;
&:hover {
background: var(--accent);
color: var(--background);
:global(svg) {
fill: var(--background);
}
}
}
}
}
</style>

88
src/lib/Hero.svelte Normal file
View File

@@ -0,0 +1,88 @@
<script>
import Icon from '$lib/Icon.svelte';
</script>
<div class="hero">
<header>
<h1>Portainer Templates</h1>
<p class="sub-title">The largest single collection, of ready-to-go Portainer templates</p>
</header>
<section class="cta">
<a href="https://github.com/Lissy93/portainer-templates">
<Icon name="github" width="26px" height="26px" />
View on GitHub
</a>
<a href="https://github.com/Lissy93/portainer-templates">
<Icon name="portainer" width="26px" height="26px" />
Install on Portainer
</a>
</section>
</div>
<style lang="scss">
.hero {
padding: 2rem;
header {
h1 {
text-align: center;
font-size: 4rem;
margin: 0 auto;
background: var(--gradient);
background-clip: border-box;
-moz-background-clip: text;
-webkit-background-clip: text;
background-clip: text;
-moz-text-fill-color: transparent;
-webkit-text-fill-color: transparent;
color: transparent;
}
.sub-title {
text-align: center;
margin: 0 auto;
font-size: 1.4rem;
font-style: italic;
font-weight: 200;
}
}
section.cta {
margin: 1rem auto;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
a {
font-size: 1.2rem;
transition:all 0.3s ease-in-out;
position: relative;
border-radius: 6px;
background: var(--background);
background-clip: padding-box;
padding: 0.5rem 1rem;
cursor: pointer;
display: flex;
gap: 1rem;
align-items: center;
min-width: 13rem;
color: var(--foreground);
text-decoration: none;
&::after {
position: absolute;
top: -4px; bottom: -4px;
left: -4px; right: -4px;
background: var(--gradient);
content: '';
z-index: -1;
border-radius: 6px;
}
&:hover {
background: var(--gradient);
transform: scale(1.03) rotate(-0.6deg);
}
}
}
}
</style>

103
src/lib/Icon.svelte Normal file

File diff suppressed because one or more lines are too long

38
src/lib/ListFilter.svelte Normal file
View File

@@ -0,0 +1,38 @@
<script lang="ts">
export let searchTerm: string;
</script>
<div class="title-row">
<h2>Template List</h2>
<div class="filters">
<input type="text" placeholder="Search..." bind:value={searchTerm} />
</div>
</div>
<style lang="scss">
.title-row {
display: flex;
align-items: center;
justify-content: space-between;
margin: 1rem auto;
padding: 0 1rem;
max-width: var(--max-width);
h2 {
font-size: 2rem;
margin: 0;
}
.filters {
input {
background: var(--card);
border: 1px solid transparent;
color: var(--foreground);
padding: 0.5rem 0.75rem;
border-radius: 6px;
transition:all 0.3s ease-in-out;
&:focus, &:hover {
box-shadow: var(--shadow);
}
}
}
}
</style>

36
src/lib/NoResults.svelte Normal file
View File

@@ -0,0 +1,36 @@
<div class="nout">
<h3>No Results 😢</h3>
<p>
<i>There weren't any templates found that matched the currently applied filters.</i><br><br>
Check the raw <a href="https://github.com/Lissy93/portainer-templates/blob/main/templates.json"><code>templates.json</code></a> file
to see all results.
If you still can't find what you're looking for, why not
<a href="https://github.com/Lissy93/portainer-templates#editing">submit a template</a>?
Feel free to raise a ticket if you need support.
</p>
</div>
<style lang="scss">
.nout {
background: var(--card);
border-radius: 6px;
min-height: 8rem;
margin: 1rem auto 5rem auto;
padding: 1rem;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
max-width: 650px;
h3 {
margin: 0;
font-size: 3rem;
}
p {
font-size: 1.1rem;
code, a {
color: var(--accent);
}
}
}
</style>

View File

@@ -0,0 +1,53 @@
<script lang="ts">
export let searchTerm: string;
export let selectedCategories: string[];
export let clearSearch: () => void;
export let numResults: number;
export let totalResults: number;
</script>
<div class="search-summary">
{#if searchTerm}
<p>
Showing {numResults} of {totalResults}
results, matching "<i>{searchTerm}</i>"
{selectedCategories.length ? `in categories: ${selectedCategories.join(', ')}` : ''}
</p>
{:else if selectedCategories.length}
<p>
Showing {numResults} of {totalResults}
results, matching categories: {selectedCategories.join(', ')}
</p>
{/if}
{#if searchTerm || selectedCategories.length}
<button on:click={clearSearch}> Clear Filters</button>
{/if}
</div>
<style lang="scss">
.search-summary {
margin: 0 1rem;
font-size: 0.9rem;
display: flex;
gap: 1rem;
align-items: center;
p {
opacity: 0.75;
}
button {
background: var(--gradient);
outline: none;
border: none;
padding: 0.25rem 0.5rem;
border-radius: 6px;
color: var(--foreground);
font-weight: 800;
cursor: pointer;
transition: all 0.2s ease-in-out;
&:hover {
transform: scale(1.1);
}
}
}
</style>

View File

@@ -0,0 +1,69 @@
<script lang="ts">
export let templates: any;
</script>
<section class="templates">
{#each templates as template}
<div class="template-card">
<h3>{template.title}</h3>
<div class="template-summary">
<div class="left">
<img src={template.logo} alt={template.title} />
</div>
<div class="txt">
<p class="description" title={template.description}>{template.description}</p>
</div>
</div>
</div>
{/each}
</section>
<style lang="scss">
section.templates {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
margin: 1rem auto;
padding: 0 1rem;
max-width: var(--max-width);
.template-card {
padding: 1rem;
border-radius: 6px;
background: var(--card);
display: flex;
flex-direction: column;
gap: 1rem;
transition:all 0.3s ease-in-out;
max-width: 28rem;
&:hover {
box-shadow: var(--shadow);
}
.template-summary {
display: flex;
gap: 1rem;
}
.left {
.info-icons { opacity: 0.3; }
}
p, h3 {
margin: 0;
}
img {
width: 64px;
max-height: 64px;
border-radius: 6px;
}
.description {
font-style: italic;
font-weight: 200;
overflow: hidden;
word-break: break-word;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 5;
line-clamp: 5;
}
}
}
</style>

101
src/routes/+page.svelte Normal file
View File

@@ -0,0 +1,101 @@
<script lang="ts">
import Hero from '$lib/Hero.svelte';
import ListFilter from '$lib/ListFilter.svelte';
import Categories from '$lib/Categories.svelte';
import SearchSummary from '$lib/SearchSummary.svelte';
import Templates from '$lib/TemplateList.svelte';
import NoResults from '$lib/NoResults.svelte';
import Footer from '$lib/Footer.svelte';
export let data;
let searchTerm = '';
let selectedCategories: string[] = [];
$: filteredTemplates = data.templates.filter((template: any) => {
const compareStr = (str1: string, str2: string) =>
(str1 || '').toLowerCase().includes(str2.toLowerCase());
if (selectedCategories.length) {
const templateCategories = template.categories || [];
const hasSelectedCategory = selectedCategories.some((cat) =>
templateCategories.includes(cat)
);
if (!hasSelectedCategory) return false;
}
return (
compareStr(template.title, searchTerm) ||
compareStr(template.description, searchTerm) ||
compareStr((template.categories || []).join(''), searchTerm)
);
});
const toggleCategory = (category: string) => {
if (selectedCategories.includes(category)) {
selectedCategories = selectedCategories.filter((cat) => cat !== category);
} else {
selectedCategories = [...selectedCategories, category];
}
};
const clearSearch = () => {
searchTerm = '';
selectedCategories = [];
}
</script>
<!-- Main title, and CTA buttons -->
<Hero />
<!-- Search bar, and Templates sub-title -->
<ListFilter bind:searchTerm={searchTerm} />
<!-- List of categories to filter by -->
<Categories
categories={data.categories}
selectedCategories={selectedCategories}
toggleCategory={toggleCategory}
/>
<!-- Text showing num results, and users search term + filters -->
<SearchSummary
searchTerm={searchTerm}
selectedCategories={selectedCategories}
clearSearch={clearSearch}
numResults={filteredTemplates.length}
totalResults={data.templates.length}
/>
<!-- List of available templates (filtered, if needed) -->
<Templates templates={filteredTemplates} />
<!-- If there are no templates matching search term, show lil message -->
{#if !filteredTemplates.length}
<NoResults />
{/if}
<!-- Footer showing license and source code links -->
<Footer />
<style lang="scss">
@import url('https://fonts.googleapis.com/css2?family=Kanit:wght@200;400;800&display=swap');
:global(body) {
--background: #101828;
--foreground: #ffffff;
--accent: #0ba5ec;
--card: #1d2939;
--shadow: 1px 1px 3px 3px #0B9AEC8F;
--gradient: linear-gradient(to right,#0B9AEC 0%,#6EDFDE 100%);
--max-width: 1800px;
margin: 0;
font-family: 'Kanit', sans-serif;
color: var(--foreground);
background: var(--background);
}
:global(::selection) {
background: var(--card);
color: var(--accent);
}
</style>

30
src/routes/+page.ts Normal file
View File

@@ -0,0 +1,30 @@
const makeCategories = (templates) => {
// Get categories from templates
const categories = templates.reduce((acc, { categories: templateCategories }) => {
(templateCategories || []).forEach((category) => {
acc[category] = (acc[category] || 0) + 1;
});
return acc;
}, {});
// Sort categories by count, and remove categories with only 1 template
const sortedCategories = Object.fromEntries(
Object.entries(categories)
.filter(([, value]) => value > 3)
.sort(([, a], [, b]) => b - a)
);
return sortedCategories;
};
export const load = async () => {
const url = 'https://raw.githubusercontent.com/Lissy93/portainer-templates/main/templates.json';
const data = await fetch(url).then((res) => res.json());
return {
templates: data.templates,
categories: makeCategories(data.templates),
}
};

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

18
svelte.config.js Normal file
View File

@@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/kit/vite';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

17
tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

6
vite.config.ts Normal file
View File

@@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});