feat: add basic overview page
This commit is contained in:
committed by
Mathias Schopmans
parent
38a59b029b
commit
1f3e1045c3
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
8
src/components/Filter/Filter.module.css
Normal file
8
src/components/Filter/Filter.module.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.filter {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
25
src/components/Filter/Filter.tsx
Normal file
25
src/components/Filter/Filter.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
src/components/Filter/QueryFilter.module.css
Normal file
25
src/components/Filter/QueryFilter.module.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/components/Filter/QueryFilter.tsx
Normal file
29
src/components/Filter/QueryFilter.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/components/Filter/RingFilter.module.css
Normal file
8
src/components/Filter/RingFilter.module.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.filter {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
43
src/components/Filter/RingFilter.tsx
Normal file
43
src/components/Filter/RingFilter.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
59
src/pages/overview.tsx
Normal 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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user