generated from nhcarrigan/template
### Explanation This creates an interactive product directory to help potential consumers discover our works. ### Issue _No response_ ### Attestations - [x] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/) - [x] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/). - [x] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/). ### Dependencies - [x] I have pinned the dependencies to a specific patch version. ### Style - [x] I have run the linter and resolved any errors. - [x] My pull request uses an appropriate title, matching the conventional commit standards. - [x] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request. ### Tests - [ ] My contribution adds new code, and I have added tests to cover it. - [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes. - [ ] All new and existing tests pass locally with my changes. - [ ] Code coverage remains at or above the configured threshold. ### Documentation _No response_ ### Versioning _No response_ Reviewed-on: #2 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #2.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
a.product {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.product:hover {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.product:not(a) {
|
||||
cursor: default;
|
||||
border: 2px dashed grey;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background-color: var(--foreground);
|
||||
color: var(--background);
|
||||
text-decoration: none;
|
||||
border-radius: 50px;
|
||||
border: 2px solid white;
|
||||
font-family: 'OpenDyslexic', monospace;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
.product {
|
||||
display: grid;
|
||||
grid-template-areas: "logo title icon" "logo description icon";
|
||||
grid-template-columns: 100px 1fr auto;
|
||||
background-color: var(--foreground);
|
||||
color: var(--background);
|
||||
border: 2px solid white;
|
||||
border-radius: 50px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding-right: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icons {
|
||||
grid-area: icon;
|
||||
font-size: 2rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, auto);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
grid-area: title;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.description {
|
||||
grid-area: description;
|
||||
font-size: 1rem;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
grid-area: logo;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<h1>Products</h1>
|
||||
<img
|
||||
src="https://cdn.nhcarrigan.com/new-avatars/hikari-thinking-full.png"
|
||||
alt="Hikari"
|
||||
height="250"
|
||||
/>
|
||||
<p>Excellent! What sort of product are you looking for?</p>
|
||||
<div class="row">
|
||||
<button
|
||||
class="btn"
|
||||
(click)="selectCategory('community')"
|
||||
[disabled]="view === 'community' ? true : false"
|
||||
>
|
||||
Community Tooling and Integrations
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
(click)="selectCategory('websites')"
|
||||
[disabled]="view === 'websites' ? true : false"
|
||||
>
|
||||
Websites and APIs
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
(click)="selectCategory('apps')"
|
||||
[disabled]="view === 'apps' ? true : false"
|
||||
>
|
||||
Apps and Games
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
(click)="selectCategory('all')"
|
||||
[disabled]="view === 'all' ? true : false"
|
||||
>
|
||||
Show Me Everything!
|
||||
</button>
|
||||
</div>
|
||||
<p>And would you like to apply a filter?</p>
|
||||
<div class="row">
|
||||
<button class="btn" (click)="toggleFilter('wip')">
|
||||
<span *ngIf="filters.wip">Hide</span
|
||||
><span *ngIf="!filters.wip">Show</span> WIP
|
||||
</button>
|
||||
<button class="btn" (click)="toggleFilter('prod')">
|
||||
<span *ngIf="filters.prod">Hide</span
|
||||
><span *ngIf="!filters.prod">Show</span> Production
|
||||
</button>
|
||||
<button class="btn" (click)="toggleFilter('paid')">
|
||||
<span *ngIf="filters.paid">Hide</span
|
||||
><span *ngIf="!filters.paid">Show</span> Paid
|
||||
</button>
|
||||
<button class="btn" (click)="toggleFilter('free')">
|
||||
<span *ngIf="filters.free">Hide</span
|
||||
><span *ngIf="!filters.free">Show</span> Free
|
||||
</button>
|
||||
</div>
|
||||
<hr />
|
||||
<p *ngIf="products.length === 0">
|
||||
Oh dear, it appears there are no products in this category yet! Please check
|
||||
back later.
|
||||
</p>
|
||||
<div id="products">
|
||||
<div *ngFor="let product of products">
|
||||
<!-- Render as <a> if product has a URL -->
|
||||
<a
|
||||
*ngIf="product.url"
|
||||
[class]="product.wip ? 'product wip' : 'product'"
|
||||
[href]="product.url"
|
||||
target="_blank"
|
||||
>
|
||||
<h2 class="title">{{ product.name }}</h2>
|
||||
<img
|
||||
class="logo"
|
||||
[src]="product.avatar ?? 'https://cdn.nhcarrigan.com/logo.png'"
|
||||
alt="{{ product.name }} Logo"
|
||||
/>
|
||||
<p class="description">{{ product.description }}</p>
|
||||
<div class="icons">
|
||||
<i
|
||||
title="Under construction"
|
||||
*ngIf="product.wip"
|
||||
class="fa-solid fa-wrench"
|
||||
style="color: rgb(141, 23, 23)"
|
||||
></i>
|
||||
<i
|
||||
title="Production Ready"
|
||||
*ngIf="!product.wip"
|
||||
class="fa-solid fa-check"
|
||||
style="color: rgb(31, 117, 19)"
|
||||
></i>
|
||||
<i
|
||||
title="Requires Subscription"
|
||||
*ngIf="product.premium"
|
||||
class="fa-solid fa-money-bill-1-wave"
|
||||
style="color: rgb(145, 129, 40)"
|
||||
></i>
|
||||
<i
|
||||
title="Free to Use"
|
||||
*ngIf="!product.premium"
|
||||
class="fa-solid fa-piggy-bank"
|
||||
style="color: rgb(116, 37, 206)"
|
||||
></i>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Render as <div> if no URL -->
|
||||
<div *ngIf="!product.url" [class]="product.wip ? 'product wip' : 'product'">
|
||||
<h2 class="title">{{ product.name }}</h2>
|
||||
<img
|
||||
class="logo"
|
||||
[src]="product.avatar ?? 'https://cdn.nhcarrigan.com/logo.png'"
|
||||
alt="{{ product.name }} Logo"
|
||||
/>
|
||||
<p class="description">{{ product.description }}</p>
|
||||
<div *ngIf="product.wip || product.premium" class="icons">
|
||||
<i
|
||||
title="Under construction"
|
||||
*ngIf="product.wip"
|
||||
class="fa-solid fa-wrench"
|
||||
style="color: rgb(141, 23, 23)"
|
||||
></i>
|
||||
<i
|
||||
title="Production Ready"
|
||||
*ngIf="!product.wip"
|
||||
class="fa-solid fa-check"
|
||||
style="color: rgb(31, 117, 19)"
|
||||
></i>
|
||||
<i
|
||||
title="Requires Subscription"
|
||||
*ngIf="product.premium"
|
||||
class="fa-solid fa-money-bill-1-wave"
|
||||
style="color: rgb(145, 129, 40)"
|
||||
></i>
|
||||
<i
|
||||
title="Free to Use"
|
||||
*ngIf="!product.premium"
|
||||
class="fa-solid fa-piggy-bank"
|
||||
style="color: rgb(116, 37, 206)"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<a
|
||||
href="https://forms.nhcarrigan.com/form/XRlQjeu8CbMrTA-v0IPOxlUPEPitLKXTWg70UUCIORA"
|
||||
target="_blank"
|
||||
class="btn"
|
||||
>I want something custom...</a
|
||||
>
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { products } from "../config/products.js";
|
||||
|
||||
@Component({
|
||||
imports: [ CommonModule ],
|
||||
selector: "app-products",
|
||||
styleUrl: "./products.css",
|
||||
templateUrl: "./products.html",
|
||||
})
|
||||
export class Products {
|
||||
public view: (typeof products)[number]["category"] | "all"
|
||||
= "all";
|
||||
public products: typeof products = [];
|
||||
public readonly filters: {
|
||||
wip: boolean;
|
||||
prod: boolean;
|
||||
paid: boolean;
|
||||
free: boolean;
|
||||
} = {
|
||||
free: true,
|
||||
paid: true,
|
||||
prod: true,
|
||||
wip: true,
|
||||
};
|
||||
|
||||
public constructor() {
|
||||
this.selectCategory("all");
|
||||
}
|
||||
|
||||
public selectCategory(
|
||||
category: (typeof products)[number]["category"] | "all",
|
||||
): void {
|
||||
this.view = category;
|
||||
const sortedProducts = products.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
if (this.view === "all") {
|
||||
this.products = sortedProducts;
|
||||
return;
|
||||
}
|
||||
this.products = sortedProducts.filter((product) => {
|
||||
return product.category === this.view;
|
||||
});
|
||||
}
|
||||
|
||||
public toggleFilter(
|
||||
filter: "wip" | "prod" | "paid" | "free",
|
||||
): void {
|
||||
this.filters[filter] = !this.filters[filter];
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
private applyFilters(): void {
|
||||
this.selectCategory(this.view);
|
||||
this.products = this.products.filter((product) => {
|
||||
if (!this.filters.wip && product.wip) {
|
||||
return false;
|
||||
}
|
||||
if (!this.filters.prod && !product.wip) {
|
||||
return false;
|
||||
}
|
||||
if (!this.filters.paid && product.premium) {
|
||||
return false;
|
||||
}
|
||||
if (!this.filters.free && !product.premium) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user