mirror of
https://github.com/LukeHagar/sveltesociety.dev.git
synced 2025-12-06 20:57:45 +00:00
Add components
This commit is contained in:
68
src/lib/components/ComponentIndex/ArrowButton.svelte
Normal file
68
src/lib/components/ComponentIndex/ArrowButton.svelte
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<script>
|
||||||
|
export const primary = false;
|
||||||
|
export let active = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
border: 2px solid #7e7e93;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: white;
|
||||||
|
color: #7e7e93;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px 15px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
div:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
border-color: var(--color) !important;
|
||||||
|
color: var(--color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
margin-left: 25px;
|
||||||
|
height: 16px;
|
||||||
|
-webkit-mask: url(/right-arrow.svg) no-repeat center;
|
||||||
|
mask: url(/right-arrow.svg) no-repeat center;
|
||||||
|
background-color: #7e7e93;
|
||||||
|
}
|
||||||
|
div:hover .arrow {
|
||||||
|
background-color: var(--color);
|
||||||
|
}
|
||||||
|
.arrow.active {
|
||||||
|
background-color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popin {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
left: calc(100% - 1rem);
|
||||||
|
top: 1rem;
|
||||||
|
z-index: 100;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 2px solid var(--color);
|
||||||
|
border-radius: 5px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
div:hover .popin:not(:empty) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
.popin {
|
||||||
|
left: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
top: 100%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div on:click>
|
||||||
|
<span>
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
<div class="arrow" class:active />
|
||||||
|
<span class="popin"><slot name="menu"></slot></span>
|
||||||
|
</div>
|
||||||
97
src/lib/components/ComponentIndex/Card.svelte
Normal file
97
src/lib/components/ComponentIndex/Card.svelte
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<script>
|
||||||
|
import Tag from "../Tag.svelte";
|
||||||
|
export let active = false;
|
||||||
|
export let title = "";
|
||||||
|
export let description = "";
|
||||||
|
export let image = "";
|
||||||
|
export let tags = [];
|
||||||
|
export let stars = 0;
|
||||||
|
export let addedOn = new Date();
|
||||||
|
export let url = "";
|
||||||
|
export let npm = "";
|
||||||
|
export let repo = "";
|
||||||
|
|
||||||
|
const copyToClipboard = (text) => {
|
||||||
|
navigator.permissions.query({name: "clipboard-write"}).then(result => {
|
||||||
|
if (result.state == "granted" || result.state == "prompt") {
|
||||||
|
navigator.clipboard.writeText(text)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: var(--width-card);
|
||||||
|
padding: 14px;
|
||||||
|
background: #f3f6f9;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.card h1 {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.active,
|
||||||
|
.card:hover {
|
||||||
|
background: #e8f3fe;
|
||||||
|
}
|
||||||
|
.card__tags {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.card__bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.card__bottom > * {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-grow {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 400px) {
|
||||||
|
.card {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="card" class:active id="component-{escape(title)}">
|
||||||
|
{#if image}
|
||||||
|
<img src={image} alt={title} />
|
||||||
|
{/if}
|
||||||
|
<h1>
|
||||||
|
<a href={url}>{title}</a> <a href="#component-{escape(title)}">#</a>
|
||||||
|
{#if npm}<Tag click={() => copyToClipboard(`npm install ${npm}`)} variant="copy" title="npm install {npm}"/>{/if}
|
||||||
|
</h1>
|
||||||
|
<p class="flex-grow">{description}</p>
|
||||||
|
<div class="card__tags">
|
||||||
|
{#each tags as tag}
|
||||||
|
<Tag title={tag} variant='blue' />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="card__bottom">
|
||||||
|
<div>
|
||||||
|
{#if stars > 0}
|
||||||
|
{#if (repo || url).includes('github')}
|
||||||
|
<img src="/github_logo.svg" alt="github logo" />
|
||||||
|
{:else if (repo || url).includes('gitlab')}
|
||||||
|
<img src="/gitlab_logo.svg" alt="gitlab logo" />
|
||||||
|
{:else}
|
||||||
|
★
|
||||||
|
{/if}
|
||||||
|
{stars}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div>{new Intl.DateTimeFormat('en-Us').format(Date.parse(addedOn))}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
45
src/lib/components/ComponentIndex/CardList.svelte
Normal file
45
src/lib/components/ComponentIndex/CardList.svelte
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<script>
|
||||||
|
export let title;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h1 {
|
||||||
|
font-family: Overpass;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 27px;
|
||||||
|
line-height: 150%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
grid-gap: 25px;
|
||||||
|
}
|
||||||
|
.list {
|
||||||
|
margin-bottom: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
.grid {
|
||||||
|
max-width: calc(var(--width-card) * 2 + 25px);
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
.grid {
|
||||||
|
max-width: var(--width-card);
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="list">
|
||||||
|
<h1 id="category-{escape(title)}">{title} <a href="#{escape(title)}">#</a></h1>
|
||||||
|
<div class="grid">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
38
src/lib/components/Tag.svelte
Normal file
38
src/lib/components/Tag.svelte
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script>
|
||||||
|
export let title = "";
|
||||||
|
export let variant;
|
||||||
|
export let click = undefined
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
/* max-width: 33%; */
|
||||||
|
padding: 2px 12px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-family: Overpass;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 150%;
|
||||||
|
text-align: center;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red {
|
||||||
|
color: #ff3e01;
|
||||||
|
background: #ffdbcf;
|
||||||
|
}
|
||||||
|
.blue {
|
||||||
|
color: #158fff;
|
||||||
|
background: #ddefff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy {
|
||||||
|
color: #ff6f01;
|
||||||
|
background: #ffe9cf;
|
||||||
|
cursor: copy;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div on:click={click} class={variant}>{title}</div>
|
||||||
24
src/routes/components/components.js
Normal file
24
src/routes/components/components.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const components = new Array(6)
|
||||||
|
.fill({
|
||||||
|
image: "/preview-image.png",
|
||||||
|
title: "svelte-tooltip",
|
||||||
|
description:
|
||||||
|
"A tooltip action that you can attach to any element and pass in a Svelte component...",
|
||||||
|
tags: [],
|
||||||
|
stars: 511,
|
||||||
|
addedOn: new Date(),
|
||||||
|
category: "forms"
|
||||||
|
})
|
||||||
|
.concat(
|
||||||
|
new Array(3).fill({
|
||||||
|
image: "/preview-image.png",
|
||||||
|
title: "svelte-ui",
|
||||||
|
description:
|
||||||
|
"A tooltip action that you can attach to any element and pass in a Svelte component...",
|
||||||
|
tags: [],
|
||||||
|
stars: 511,
|
||||||
|
addedOn: new Date(),
|
||||||
|
category: "ui"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
export default components;
|
||||||
3956
src/routes/components/components.json
Normal file
3956
src/routes/components/components.json
Normal file
File diff suppressed because one or more lines are too long
212
src/routes/components/index.svelte
Normal file
212
src/routes/components/index.svelte
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
<script>
|
||||||
|
import ComponentCard from "$lib/components/ComponentIndex/Card.svelte";
|
||||||
|
import List from "$lib/components/ComponentIndex/CardList.svelte";
|
||||||
|
import Button from "$lib/components/ComponentIndex/ArrowButton.svelte";
|
||||||
|
import components from "./components.json";
|
||||||
|
|
||||||
|
let searchValue;
|
||||||
|
let searchTag;
|
||||||
|
const tags = Array.from(new Set(components.map(item => item.tags).flat()))
|
||||||
|
const allCategories = Array.from(new Set(components.map(item => item.category).flat()))
|
||||||
|
let filterTag = []
|
||||||
|
let filterCategory = null
|
||||||
|
let sorting = 'added_desc';
|
||||||
|
|
||||||
|
const intersection = (array1, array2) => {
|
||||||
|
return array1.filter(item => array2.includes(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
$: dataToDisplay = components.filter(component => {
|
||||||
|
if (!searchValue && filterTag.length === 0 && filterCategory === null) return true
|
||||||
|
|
||||||
|
if (
|
||||||
|
(searchValue && !(component.title.toLowerCase().includes(searchValue.toLowerCase()) || component.description.toLowerCase().includes(searchValue.toLowerCase())))
|
||||||
|
|| (filterTag.length > 0 && intersection(filterTag, component.tags).length === 0)
|
||||||
|
|| (filterCategory !== null && component.category !== filterCategory)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}).sort((componentA, componentB) => {
|
||||||
|
switch (sorting) {
|
||||||
|
case "added_desc": return new Date(componentB.addedOn || '').getTime() - new Date(componentA.addedOn || '').getTime()
|
||||||
|
case "added_asc": return new Date(componentA.addedOn || '').getTime() - new Date(componentB.addedOn || '').getTime()
|
||||||
|
case "name_asc": return componentA.title.toLowerCase().localeCompare(componentB.title.toLowerCase())
|
||||||
|
case "name_desc": return componentB.title.toLowerCase().localeCompare(componentA.title.toLowerCase())
|
||||||
|
case "stars_desc": return (componentB.stars || 0) - (componentA.stars || 0)
|
||||||
|
case "stars_asc": return (componentA.stars || 0) - (componentB.stars || 0)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$: tagSearchResult = searchTag ? tags.filter(item => item.includes(searchTag)) : tags
|
||||||
|
$: categories = Array.from(new Set(dataToDisplay.map(item => item.category)))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-family: Overpass;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, auto);
|
||||||
|
grid-gap: 0.5rem;
|
||||||
|
margin-right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchbar {
|
||||||
|
height: 100%;
|
||||||
|
width: 35%;
|
||||||
|
font-family: Overpass;
|
||||||
|
border-width: 0;
|
||||||
|
background: #f3f6f9 url(/search-icon.svg) 98% no-repeat;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchbar-count {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.popin {
|
||||||
|
padding: 0.5ex;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.7em;
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.popin li {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
ul.popin li:hover {
|
||||||
|
background: #eee;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
ul.popin li.tag-search {
|
||||||
|
position: sticky;
|
||||||
|
top: -0.5ex;
|
||||||
|
margin: 0 -0.5ex;
|
||||||
|
padding: 0.5ex;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
ul.popin li.tag-search input {
|
||||||
|
margin: 0;
|
||||||
|
background: #f3f6f9;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 15ch;
|
||||||
|
}
|
||||||
|
ul.popin li.tag-search:hover {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
ul.popin.no-wrap li {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.popin li label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 0.7rem;
|
||||||
|
padding: 0.8ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.popin li input {
|
||||||
|
flex: 0;
|
||||||
|
margin: 0 1ex 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
.controls {
|
||||||
|
flex-flow: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputs {
|
||||||
|
align-self: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
grid-template-columns: repeat(3, auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchbar {
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-bottom: 1ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
.controls {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputs {
|
||||||
|
grid-template-columns: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchbar {
|
||||||
|
width: auto;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<h1>Components</h1>
|
||||||
|
<div class="controls">
|
||||||
|
<div class="inputs">
|
||||||
|
<Button active={filterTag.length > 0}>
|
||||||
|
Tags {#if filterTag.length > 0}<small>({filterTag.length})</small>{/if}
|
||||||
|
<ul slot="menu" role="menu" class="popin">
|
||||||
|
<li class="tag-search"><input placeholder="Search for tags..." bind:value={searchTag} /></li>
|
||||||
|
{#each tagSearchResult as tag}
|
||||||
|
<li><label><input type="checkbox" bind:group={filterTag} value={tag}> {tag}</label></li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</Button>
|
||||||
|
<Button active={filterCategory !== null}>
|
||||||
|
Categories
|
||||||
|
<ul slot="menu" role="menu" class="popin">
|
||||||
|
<li><label><input type="radio" bind:group={filterCategory} value={null}> All</label></li>
|
||||||
|
{#each allCategories as category}
|
||||||
|
<li><label><input type="radio" bind:group={filterCategory} value={category}> {category||"Unclassified"}</label></li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</Button>
|
||||||
|
<Button on:click={() => location.href = "/help/components"}>Submit a component</Button>
|
||||||
|
<Button active={sorting !== ''}>
|
||||||
|
Sort
|
||||||
|
<ul slot="menu" role="menu" class="popin no-wrap">
|
||||||
|
<li><label><input type="radio" bind:group={sorting} value="added_desc"> Last Added first</label></li>
|
||||||
|
<li><label><input type="radio" bind:group={sorting} value="added_asc"> Oldest first</label></li>
|
||||||
|
<li><label><input type="radio" bind:group={sorting} value="stars_desc"> Stars Desc</label></li>
|
||||||
|
<li><label><input type="radio" bind:group={sorting} value="stars_asc"> Stars Asc</label></li>
|
||||||
|
<li><label><input type="radio" bind:group={sorting} value="name_asc"> Name Asc</label></li>
|
||||||
|
<li><label><input type="radio" bind:group={sorting} value="name_desc"> Name Desc</label></li>
|
||||||
|
</ul>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="searchbar"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search for components..."
|
||||||
|
bind:value={searchValue} />
|
||||||
|
<span class="searchbar-count">{dataToDisplay.length} result{#if dataToDisplay.length !== 1}s{/if}</span>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
{#each categories as category}
|
||||||
|
<List title={category||"Unclassified"}>
|
||||||
|
{#each dataToDisplay.filter(d => d.category === category) as data}
|
||||||
|
<ComponentCard {...data} />
|
||||||
|
{/each}
|
||||||
|
</List>
|
||||||
|
{/each}
|
||||||
|
</main>
|
||||||
Reference in New Issue
Block a user