feat(redesign): Download page

This commit is contained in:
darkweak 2023-10-14 00:54:11 +02:00 committed by Francis Lavoie
parent 7294fc7309
commit 982393ff91
No known key found for this signature in database
GPG key ID: C5204D4F28147FC8
4 changed files with 415 additions and 8 deletions

View file

@ -4,21 +4,73 @@
<title>Download Caddy</title> <title>Download Caddy</title>
{{include "/includes/head.html"}} {{include "/includes/head.html"}}
<link rel="stylesheet" href="/resources/css/download.css"> <link rel="stylesheet" href="/resources/css/download.css">
<link rel="stylesheet" href="/resources/css/docs.css">
<script src="/resources/js/download.js"></script>
</head> </head>
<body> <body>
<div class="hero"> {{include "/includes/header.html" "light-header"}}
{{include "/includes/header.html" "light-header"}}
<main> <main>
<article>
<div class="wrapper"> <h1>Download</h1>
Hello, world! </article>
<div class="wrapper filters">
<input id="search-package" placeholder="Search package: e.g. cloudflare" class="shadow" />
<select id="sort-package" class="shadow">
<option value="download">Sort by most popular</option>
<option value="alphabetically">Sort alphabetically</option>
<option value="type">Group by module namespace</option>
</select>
</div>
<div class="wrapper list">
<div id="side-panel-packages">
</div>
<div id="packages">
</div>
</div> </div>
</main> </main>
{{include "/includes/footer.html"}} {{include "/includes/footer.html"}}
<script type="text/javascript">
let groupBy = 'download';
const getCardTemplate = item => `{{ include "/includes/card.html" }}`;
function renderList(list) {
if (groupBy === 'type') {
const groupedData = packageManager.group(groupBy)
document.getElementById('side-panel-packages').innerHTML = `
<div>
${Object.keys(groupedData).map(k => `<a href="#${k}"> ${ k }</a>`).join('')}
</div>`;
document.getElementById('packages').innerHTML = Object.entries(groupedData).filter(([_, items]) => !!items.length).map(([category, items]) => `
<section id="${category}">
<h2 class="blue">${category}</h2>
<div class="card-list">${items.map(getCardTemplate).join('')}</div>
</section>`).join('')
return;
}
document.getElementById('side-panel-packages').innerHTML = '';
document.getElementById('packages').innerHTML = `
<div class="card-list">
${list.map(getCardTemplate).join('')}
</div>`;
};
packageManager.getPackages().then(() => {
renderList(packageManager.group(groupBy))
})
document.getElementById('search-package').addEventListener('input', ({ target: { value } }) => {
packageManager.setFilterValue(value.toLowerCase());
renderList(packageManager.group(groupBy))
})
document.getElementById('sort-package').addEventListener('change', ({ target: { value } }) => {
groupBy = value;
renderList(packageManager.group(value))
})
</script>
</body> </body>
</html> </html>

46
new/includes/card.html Normal file
View file

@ -0,0 +1,46 @@
<div class="card shadow">
<div class="card-header flex">
<div class="card-title-name">
<h3>
${item.name}
</h3>
<a href="${item.repo}" target="_blank" rel="noopener noreferer">
${item.path}
</a>
</div>
<div class="card-title-info">
<span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-download" width="24"
height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2"></path>
<path d="M7 11l5 5l5 -5"></path>
<path d="M12 4l0 12"></path>
</svg>
${item.downloads}
</span>
<span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-git-commit" width="24"
height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
<path d="M12 3l0 6"></path>
<path d="M12 15l0 6"></path>
</svg>
latest
</span>
</div>
</div>
<div class="card-description">
<p>
${item.description}
</p>
<div class="card-actions">
<button type="button" class="button card-button">
Add this module
</button>
</div>
</div>
</div>

View file

@ -0,0 +1,189 @@
.card {
border: 1px solid rgba(226, 232, 240, 0.8);
border-radius: 16px;
padding: 16px;
width: 100%;
height: 100%;
background-color: var(--body-bg);
display: flex;
flex-direction: column;
gap: 16px;
}
html {
scroll-behavior: smooth;
}
.shadow {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06);
}
.wrapper.list {
display: flex;
}
#side-panel-packages {
padding-top: 32px;
padding-bottom: 32px;
}
#side-panel-packages>div {
padding: 1rem 1rem 1rem 0;
overflow-y: scroll;
width: 300px;
max-height: 100vh;
display: flex;
flex-direction: column;
position: sticky;
top: 0;
}
#packages {
width: 100%;
padding-top: 32px;
padding-bottom: 32px;
display: grid;
gap: 48px;
}
#packages>section>h2 {
padding-bottom: 16px;
color: rgb(14, 110, 189);
border-color: rgb(14, 110, 189);
}
.card-list {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 32px;
}
.card-header {
width: 100%;
display: flex;
justify-content: space-between;
}
.card-title {
width: 100%;
display: flex;
padding-left: 16px;
justify-content: space-between;
}
.card-title-name {
display: flex;
flex-direction: column;
font-weight: lighter;
color: var(--text-color);
}
.card-title-info {
display: inline-flex;
gap: 16px;
font-size: 90%;
color: var(--text-color-muted);
}
.card-title-info>span>svg {
margin-right: -4px;
}
.card-header>:first-child>span:first-child {
font-weight: bold;
}
.card-icon {
border-radius: 8px;
}
.card-description {
display: flex;
color: var(--text-color-muted);
justify-content: space-between;
height: 100%;
gap: 8px;
}
.card-description>p {
font-weight: 400;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
overflow: hidden;
word-break: break-word;
}
.card-actions {
margin-top: auto;
}
.card-button {
margin: 0 auto;
min-width: fit-content;
white-space: nowrap;
font-size: 80%;
font-weight: bold;
}
article {
margin-left: auto;
margin-right: auto;
}
.filters {
display: grid;
grid-template-columns: 2fr 1fr 2fr;
gap: 16px;
}
@media (max-width: 1400px) {
.card-description {
flex-direction: column;
}
.card-list {
grid-template-columns: 1fr;
}
.filters {
grid-template-columns: 1fr;
}
}
input,
select {
padding: 0.5rem;
height: 3rem;
font-size: 100%;
font-weight: 600;
background-color: white;
width: 100%;
border-radius: 0.5rem;
border: none;
}
.filters>div {
flex-direction: column;
}
select {
cursor: pointer;
-webkit-user-select: none;
user-select: none;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
padding-left: 1rem;
padding-right: 2.5rem;
font-size: .875rem;
line-height: 1.25rem;
line-height: 2;
min-height: 3rem;
border-width: 1px;
border-color: var(--text-color-muted);
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%);
background-position: calc(100% - 20px) calc(1px + 50%), calc(100% - 16.1px) calc(1px + 50%);
background-size: 4px 4px, 4px 4px;
background-repeat: no-repeat;
}

View file

@ -0,0 +1,120 @@
class Package {
/**
* @typedef {Object} Module
* @property {string} docs
* @property {string} name
* @property {string} package
* @property {string} repo
*/
/**
* @typedef {Object} Pkg
* @property {string} id
* @property {string} path
* @property {string} published
* @property {boolean} listed
* @property {boolean} available
* @property {number} downloads
* @property {ReadonlyArray<Module>} modules
* @property {string} repo
* @property {string} name
*/
/** @type {string} */
pkgURL = "https://localhost/api/packages";
/**
* @type {ReadonlyArray<Pkg>}
*/
packages = [];
/**
* @type {string}
*/
filter = '';
/**
* @returns Promise<>
*/
getPackages() {
return fetch(this.pkgURL, { headers: { 'X-Requested-With': 'XMLHttpRequest', Origin: 'https://caddyserver.com' } })
.then(res => res.json())
.then(({ result }) => this.packages = result.sort((a, b) => a.downloads - b.downloads).map(item => ({ ...item, description: item.modules?.map(m => m.docs ?? m.name).join('\n') ?? '', name: item.repo.split('/')[4].toLowerCase() })));
}
setFilterValue(value) {
this.filter = value;
}
getSearchPackages(pkgs) {
if (!this.filter) {
return pkgs;
}
return pkgs.filter(pkg => pkg.name.includes(this.filter) || pkg.repo.includes(this.filter) || pkg.description.includes(this.filter));
}
/**
* @param {'alphabetically' | 'type' | 'download'} groupBy
* @return {
* Record<string, Pkg> | ReadonlyArray<Pkg}
*/
group(groupBy = 'alphabetically') {
const pkgs = this.getSearchPackages(this.packages);
switch (groupBy) {
case 'alphabetically':
return pkgs.sort((a,b) => a.name.localeCompare(b.name));
case 'download':
return pkgs.sort((a,b) => b.downloads - a.downloads);
case 'type':
return pkgs.reduce((acc, current) => {
if (!current?.modules?.length) {
return acc;
}
current.modules.forEach(module => {
let moduleName = module.name
if (module.name.includes('.')) {
const splitted = module.name.split('.')
moduleName = `${splitted[0]}.${splitted[1]}`
}
if (acc[moduleName]) {
acc[moduleName] = [...acc[moduleName], current];
}
})
return acc;
}, {
"http.handlers": [],
"http.matchers": [],
"dns.providers": [],
"http.encoders": [],
"caddy.config_loaders": [],
"caddy.fs": [],
"caddy.listeners": [],
"caddy.logging.encoders": [],
"caddy.logging.encoders.filter": [],
"caddy.logging.writers": [],
"caddy.storage": [],
"events.handlers": [],
"http.authentication.hashes": [],
"http.authentication.providers": [],
"http.ip_sources": [],
"http.precompressed": [],
"http.reverse_proxy.circuit_breakers": [],
"http.reverse_proxy.selection_policies": [],
"http.reverse_proxy.transport": [],
"http.reverse_proxy.upstreams": [],
"tls.certificates": [],
"tls.client_auth": [],
"tls.handshake_match": [],
"tls.issuance": [],
"tls.get_certificate": [],
"tls.stek": [],
})
}
}
}
const packageManager = new Package();