feat: add basic overview page

This commit is contained in:
Mathias Schopmans
2024-02-19 11:15:19 +01:00
committed by Mathias Schopmans
parent 38a59b029b
commit 1f3e1045c3
14 changed files with 249 additions and 8 deletions

View File

@@ -27,3 +27,24 @@
color: var(--foreground); color: var(--foreground);
background-color: var(--badge); background-color: var(--badge);
} }
.selectable {
cursor: pointer;
&:not(.selected) {
color: var(--foreground);
border: 1px solid var(--foreground);
background: transparent;
}
&:not(.colored) {
color: var(--foreground);
border: 1px solid var(--foreground);
background: transparent;
&.selected {
color: var(--background);
background: #fff;
}
}
}

View File

@@ -15,6 +15,8 @@ import { cn } from "@/lib/utils";
interface BadgeProps extends ComponentPropsWithoutRef<"span"> { interface BadgeProps extends ComponentPropsWithoutRef<"span"> {
children?: ReactNode; children?: ReactNode;
color?: string; color?: string;
selectable?: boolean;
selected?: boolean;
size?: "small" | "medium" | "large"; size?: "small" | "medium" | "large";
} }
@@ -22,6 +24,8 @@ export function Badge({
children, children,
color, color,
size = "medium", size = "medium",
selectable,
selected,
...props ...props
}: BadgeProps) { }: BadgeProps) {
const style = useMemo( const style = useMemo(
@@ -40,6 +44,8 @@ export function Badge({
styles.badge, styles.badge,
styles[`size-${size}`], styles[`size-${size}`],
color && styles.colored, color && styles.colored,
selectable && styles.selectable,
selected && styles.selected,
)} )}
> >
{children} {children}

View File

@@ -0,0 +1,8 @@
.filter {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
margin-bottom: 20px;
gap: 20px;
}

View File

@@ -0,0 +1,25 @@
import styles from "./Filter.module.css";
import { QueryFilter } from "@/components/Filter/QueryFilter";
import { RingFilter } from "@/components/Filter/RingFilter";
interface FilterProps {
query?: string;
onQueryChange: (query: string) => void;
ring?: string;
onRingChange: (ring: string) => void;
}
export function Filter({
query,
onQueryChange,
ring,
onRingChange,
}: FilterProps) {
return (
<div className={styles.filter}>
<QueryFilter value={query} onChange={onQueryChange} />
<RingFilter value={ring} onChange={onRingChange} />
</div>
);
}

View File

@@ -0,0 +1,25 @@
.filter {
flex: 1 1 100%;
position: relative;
}
.input {
padding-right: 50px;
}
.button {
position: absolute;
top: 50%;
right: 16px;
width: 20px;
height: 20px;
margin: -10px 0 0;
background: transparent;
border: none;
}
@media (min-width: 768px) {
.filter {
flex: 1 1 auto;
}
}

View File

@@ -0,0 +1,29 @@
import { ChangeEvent } from "react";
import Search from "../Icons/Search";
import styles from "./QueryFilter.module.css";
interface QueryFilterProps {
value?: string;
onChange: (value: string) => void;
}
export function QueryFilter({ value, onChange }: QueryFilterProps) {
const _onChange = (e: ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
};
return (
<div className={styles.filter}>
<input
className={styles.input}
type="search"
value={value}
onChange={_onChange}
/>
<button className={styles.button} type="submit">
<Search />
</button>
</div>
);
}

View File

@@ -0,0 +1,8 @@
.filter {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 20px;
}

View File

@@ -0,0 +1,43 @@
import styles from "./RingFilter.module.css";
import { Badge, RingBadge } from "@/components/Badge/Badge";
import { getRings } from "@/lib/data";
import { cn } from "@/lib/utils";
interface RingFilterProps {
value?: string;
onChange: (value: string) => void;
className?: string;
}
export function RingFilter({ value, onChange, className }: RingFilterProps) {
const rings = getRings();
return (
<ul className={cn(styles.filter, className)}>
<li>
<Badge
size="large"
selectable
selected={!value}
onClick={() => {
onChange("");
}}
>
All
</Badge>
</li>
{rings.map((ring) => (
<li key={ring.id}>
<RingBadge
ring={ring.id}
size="large"
selectable
selected={value === ring.id}
onClick={() => onChange(ring.id)}
/>
</li>
))}
</ul>
);
}

View File

@@ -9,7 +9,7 @@ import { cn } from "@/lib/utils";
export interface ItemListProps { export interface ItemListProps {
items: Item[]; items: Item[];
activeId?: string; activeId?: string;
size?: "small" | "default"; size?: "small" | "default" | "large";
className?: string; className?: string;
} }
@@ -23,6 +23,7 @@ export function ItemList({
<ul <ul
className={cn(styles.list, className, { className={cn(styles.list, className, {
[styles.isSmall]: size === "small", [styles.isSmall]: size === "small",
[styles.large]: size === "large",
})} })}
> >
{items.map((item) => ( {items.map((item) => (

View File

@@ -3,16 +3,13 @@ import Link from "next/link";
import styles from "./QuadrantList.module.css"; import styles from "./QuadrantList.module.css";
import { RingList } from "@/components/RingList/RingList"; import { RingList } from "@/components/RingList/RingList";
import { import { getQuadrant, groupItemsByQuadrant } from "@/lib/data";
getQuadrant,
groupItemsByQuadrant,
groupItemsByRing,
} from "@/lib/data";
import { Item } from "@/lib/types"; import { Item } from "@/lib/types";
interface RingListProps { interface RingListProps {
items: Item[]; items: Item[];
} }
export function QuadrantList({ items }: RingListProps) { export function QuadrantList({ items }: RingListProps) {
const quadrants = groupItemsByQuadrant(items); const quadrants = groupItemsByQuadrant(items);
return ( return (

View File

@@ -24,7 +24,7 @@ export default function App({ Component, pageProps, router }: CustomAppProps) {
<link rel="icon" href={assetUrl("/favicon.ico")} /> <link rel="icon" href={assetUrl("/favicon.ico")} />
</Head> </Head>
<Layout layoutClass={Component.layoutClass}> <Layout layoutClass={Component.layoutClass}>
<Component {...pageProps} key={router.asPath} /> <Component {...pageProps} />
</Layout> </Layout>
</> </>
); );

View File

@@ -1,4 +1,3 @@
import { ItemList } from "@/components/ItemList/ItemList";
import { QuadrantList } from "@/components/QuadrantList/QuadrantList"; import { QuadrantList } from "@/components/QuadrantList/QuadrantList";
import { getAppName, getItems, getReleases } from "@/lib/data"; import { getAppName, getItems, getReleases } from "@/lib/data";
import { CustomPage } from "@/pages/_app"; import { CustomPage } from "@/pages/_app";

59
src/pages/overview.tsx Normal file
View File

@@ -0,0 +1,59 @@
import Head from "next/head";
import { useRouter } from "next/router";
import { useCallback, useMemo } from "react";
import { Filter } from "@/components/Filter/Filter";
import { ItemList } from "@/components/ItemList/ItemList";
import { getItems } from "@/lib/data";
import { formatTitle } from "@/lib/format";
import { CustomPage } from "@/pages/_app";
const Overview: CustomPage = () => {
const router = useRouter();
const ring = router.query.ring as string | undefined;
const query = router.query.query as string | undefined;
const onRingChange = useCallback(
(ring: string) => {
router.push({ query: { ...router.query, ring, query } });
},
[router, query],
);
const onQueryChange = useCallback(
(query: string) => {
router.replace({ query: { ...router.query, ring, query } });
},
[router, ring],
);
const items = useMemo(() => {
if (!ring && !query) return getItems();
return getItems().filter((item) => {
if (ring && item.ring !== ring) return false;
return !(
query && !item.title.toLowerCase().includes(query.toLowerCase())
);
});
}, [query, ring]);
return (
<>
<Head>
<title>{formatTitle("Technologies Overview")}</title>
</Head>
<h1>Technologies Overview</h1>
<Filter
query={query}
ring={ring}
onRingChange={onRingChange}
onQueryChange={onQueryChange}
/>
<ItemList items={items} />
</>
);
};
export default Overview;

View File

@@ -18,6 +18,7 @@
* { * {
box-sizing: border-box; box-sizing: border-box;
outline-color: var(--highlight);
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
@@ -88,3 +89,22 @@ ol {
padding-left: 16px; padding-left: 16px;
margin-bottom: 1em; margin-bottom: 1em;
} }
input {
background: var(--foreground);
color: var(--background);
border: 1px solid transparent;
padding: 10px 12px;
border-radius: 6px;
width: 100%;
font-size: 16px;
&:focus {
outline: none;
border-color: var(--highlight);
}
}
input::-webkit-search-cancel-button {
display: none;
}