mirror of
https://github.com/LukeHagar/sveltesociety.dev.git
synced 2025-12-06 12:47:44 +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