mirror of
https://github.com/hsjobeki/noogle.git
synced 2024-11-24 06:35:05 +03:00
feature/deeplinks
* add: deeplinks * add: enhanced pagination for large datasets * add more meta tags * add data to initial state * remove preview page, instead link directly into the list
This commit is contained in:
parent
b9a63521af
commit
8679af38a2
1
components/NixFunctions/index.ts
Normal file
1
components/NixFunctions/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export {NixFunctions as default} from "./nixFunctions";
|
56
components/NixFunctions/nixFunctions.tsx
Normal file
56
components/NixFunctions/nixFunctions.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { Box } from "@mui/system";
|
||||
import { useMemo } from "react";
|
||||
import { PageState } from "../../models/internals";
|
||||
import { byQuery, byType, pipe } from "../../queries";
|
||||
import { DocItem } from "../../models/nix";
|
||||
import { BasicList, BasicListItem } from "../basicList";
|
||||
import FunctionItem from "../functionItem/functionItem";
|
||||
import { SetPageStateVariable } from "../pageContext";
|
||||
|
||||
interface FunctionsProps {
|
||||
pageState: PageState;
|
||||
setPageStateVariable: SetPageStateVariable;
|
||||
}
|
||||
|
||||
export function NixFunctions(props: FunctionsProps) {
|
||||
const { pageState, setPageStateVariable } = props;
|
||||
const { data, selected, term, filter } = pageState;
|
||||
|
||||
const setSelected = setPageStateVariable<string | null>("selected");
|
||||
|
||||
const filteredData = useMemo(
|
||||
() => pipe(byType(filter), byQuery(term))(data),
|
||||
[filter, term, data]
|
||||
);
|
||||
|
||||
const preRenderedItems: BasicListItem[] = filteredData.map(
|
||||
(docItem: DocItem) => {
|
||||
const key = docItem.id;
|
||||
return {
|
||||
item: (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
onClick={!(selected === key) ? () => setSelected(key) : undefined}
|
||||
>
|
||||
<FunctionItem
|
||||
name={docItem.name}
|
||||
docItem={docItem}
|
||||
selected={selected === key}
|
||||
handleClose={() => setSelected(null)}
|
||||
/>
|
||||
</Box>
|
||||
),
|
||||
key,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ ml: { xs: 0, md: 2 } }}>
|
||||
<BasicList items={preRenderedItems} />
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
import "@testing-library/jest-dom";
|
||||
// TODO: important mock zustand!
|
||||
// State from previous tests leaking into other test cases because zustand is never reset in its own
|
||||
|
||||
import * as React from "react";
|
||||
import { render, screen, act } from "@testing-library/react";
|
||||
import mockAxios from "jest-mock-axios";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { ImageList } from "./";
|
||||
import { QueryClient, QueryClientProvider } from "react-query";
|
||||
import { SnackbarProvider } from "notistack";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const Wrapper = (element: React.ReactNode) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SnackbarProvider>{element}</SnackbarProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
const getImagesResp = [
|
||||
{
|
||||
filename: "testfile.iso",
|
||||
hash: "3993d64556f3ae803986f6d1c2debaa8f5a2a77da70cc35f86af6f99c849fe80",
|
||||
modify_time: "2022-07-28T07:45:32.331998395Z",
|
||||
size_bytes: 513,
|
||||
},
|
||||
{
|
||||
filename: "full.img",
|
||||
hash: "85c6a11c928611caff965aa2fc267b51ad3fc06657d0ee67ae00fec1711e81ef",
|
||||
modify_time: "2022-07-29T07:33:20.075281078Z",
|
||||
size_bytes: 1024 * 1024 * 1024,
|
||||
},
|
||||
{
|
||||
filename: "small.img",
|
||||
hash: "e8edbf1bb6121a38e8ee3a60e34a31887955bc9a292e9ef3bd598e8ccb7fef5a",
|
||||
modify_time: "2022-07-29T07:34:10.442184606Z",
|
||||
size_bytes: 1,
|
||||
},
|
||||
];
|
||||
|
||||
describe("render imageList", () => {
|
||||
afterEach(() => {
|
||||
mockAxios.reset();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
mockAxios.get.mockResolvedValue({ data: getImagesResp });
|
||||
render(Wrapper(<ImageList />), {});
|
||||
});
|
||||
});
|
||||
|
||||
test("list is shown & has proper length", async () => {
|
||||
const list = await screen.getByRole("list", { name: "image-list" });
|
||||
const listItems = await screen.getAllByRole("listitem", {
|
||||
name: "image-entry-item",
|
||||
});
|
||||
expect(list).toBeVisible();
|
||||
expect(listItems).toHaveLength(getImagesResp.length);
|
||||
expect(mockAxios.get).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test("search: a term", async () => {
|
||||
const searchInput = await screen.getByRole("textbox", {
|
||||
name: "search-input",
|
||||
});
|
||||
const searchButton = await screen.getByRole("button", {
|
||||
name: "search-button",
|
||||
});
|
||||
await userEvent.type(searchInput, ".img");
|
||||
await userEvent.click(searchButton);
|
||||
|
||||
/*
|
||||
search for filename with ".img" in it
|
||||
-> should yield:
|
||||
[ "full.img", "small.img" ]
|
||||
*/
|
||||
const listItems = await screen.getAllByRole("listitem", {
|
||||
name: "image-entry-item",
|
||||
});
|
||||
expect(listItems).toHaveLength(2);
|
||||
expect(listItems[0]).toHaveTextContent("full.img");
|
||||
expect(listItems[1]).toHaveTextContent("small.img");
|
||||
});
|
||||
|
||||
test("delete an image", async () => {
|
||||
const deleteButtons = await screen.getAllByRole("button", {
|
||||
name: "delete-image",
|
||||
});
|
||||
|
||||
const firstDeleteButton = deleteButtons?.[0];
|
||||
|
||||
expect(firstDeleteButton).toBeEnabled();
|
||||
|
||||
await userEvent.click(firstDeleteButton);
|
||||
|
||||
//TODO: dont have delete functionality yet
|
||||
// expect(mockedDeleteFn).toBeCalled();
|
||||
});
|
||||
});
|
@ -1,72 +1,42 @@
|
||||
import React, { useState, useMemo, useEffect } from "react";
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
Pagination,
|
||||
Stack,
|
||||
Typography,
|
||||
Grid,
|
||||
Slide,
|
||||
Collapse,
|
||||
Grow,
|
||||
} from "@mui/material";
|
||||
import { BasicDataItem, BasicDataViewProps } from "../../types/basicDataView";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { Box, List, ListItem, Stack, TablePagination } from "@mui/material";
|
||||
import { BasicDataViewProps } from "../../types/basicDataView";
|
||||
import { SearchInput } from "../searchInput";
|
||||
import Radio from "@mui/material/Radio";
|
||||
import RadioGroup from "@mui/material/RadioGroup";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import FormLabel from "@mui/material/FormLabel";
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
|
||||
import { NixType, nixTypes } from "../../types/nix";
|
||||
import { Filter } from "../searchInput/searchInput";
|
||||
import { useRouter } from "next/router";
|
||||
import { FunctionOfTheDay } from "../functionOfTheDay";
|
||||
import { usePageContext } from "../pageContext";
|
||||
import { useMobile } from "../layout/layout";
|
||||
import { EmptyRecordsPlaceholder } from "../emptyRecordsPlaceholder";
|
||||
import { FunctionOfTheDay } from "../functionOfTheDay";
|
||||
|
||||
export type BasicListItem = {
|
||||
item: React.ReactNode;
|
||||
key: string;
|
||||
};
|
||||
export type BasicListProps = BasicDataViewProps & {
|
||||
handleFilter: (filter: Filter | ((curr: Filter) => Filter)) => void;
|
||||
filter: Filter;
|
||||
term: string;
|
||||
selected?: string | null;
|
||||
itemsPerPage: number;
|
||||
};
|
||||
|
||||
type ViewMode = "explore" | "browse";
|
||||
export function BasicList(props: BasicListProps) {
|
||||
const {
|
||||
items,
|
||||
pageCount = 1,
|
||||
itemsPerPage,
|
||||
handleSearch,
|
||||
handleFilter,
|
||||
selected = "",
|
||||
filter,
|
||||
term,
|
||||
} = props;
|
||||
|
||||
const [page, setPage] = useState<number>(1);
|
||||
export function BasicList(props: BasicListProps) {
|
||||
const { items } = props;
|
||||
const { pageState, setPageStateVariable, resetQueries } = usePageContext();
|
||||
const isMobile = useMobile();
|
||||
const { page, itemsPerPage, filter, term, FOTD, data } = pageState;
|
||||
const [mode, setMode] = useState<ViewMode>("explore");
|
||||
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
const { query } = router;
|
||||
if (query?.page) {
|
||||
if (typeof query.page === "string") {
|
||||
const page = Number(query.page);
|
||||
setPage(page);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router]);
|
||||
const setPage = setPageStateVariable<number>("page");
|
||||
const setTerm = setPageStateVariable<string>("term");
|
||||
const setFilter = setPageStateVariable<Filter>("filter");
|
||||
const setItemsPerPage = setPageStateVariable<number>("itemsPerPage");
|
||||
|
||||
const handleChangeRowsPerPage = (
|
||||
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
setItemsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(1);
|
||||
};
|
||||
const pageItems = useMemo(() => {
|
||||
const startIdx = (page - 1) * itemsPerPage;
|
||||
const endIdx = page * itemsPerPage;
|
||||
@ -74,41 +44,47 @@ export function BasicList(props: BasicListProps) {
|
||||
}, [page, items, itemsPerPage]);
|
||||
|
||||
const handlePageChange = (
|
||||
_event: React.ChangeEvent<unknown>,
|
||||
event: React.MouseEvent<HTMLButtonElement> | null,
|
||||
value: number
|
||||
) => {
|
||||
setPage(value);
|
||||
setPage(value + 1);
|
||||
};
|
||||
const handleClear = () => {
|
||||
resetQueries();
|
||||
};
|
||||
|
||||
const _handleFilter = (filter: Filter | ((curr: Filter) => Filter)) => {
|
||||
setMode("browse");
|
||||
handleFilter(filter);
|
||||
const handleFilter = (filter: Filter | ((curr: Filter) => Filter)) => {
|
||||
setFilter(filter);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const _handleSearch = (term: string) => {
|
||||
setMode("browse");
|
||||
handleSearch && handleSearch(term);
|
||||
|
||||
const handleSearch = (term: string) => {
|
||||
setTerm(term);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const showFunctionExplore =
|
||||
const showFunctionOfTheDay =
|
||||
mode === "explore" &&
|
||||
filter.to === "any" &&
|
||||
filter.from === "any" &&
|
||||
term === "";
|
||||
term === "" &&
|
||||
FOTD === true;
|
||||
return (
|
||||
<Stack>
|
||||
<SearchInput
|
||||
handleFilter={_handleFilter}
|
||||
handleFilter={handleFilter}
|
||||
handleClear={handleClear}
|
||||
placeholder="search nix functions"
|
||||
handleSearch={_handleSearch}
|
||||
page={page}
|
||||
clearSearch={() => _handleSearch("")}
|
||||
handleSearch={handleSearch}
|
||||
/>
|
||||
{showFunctionExplore ? (
|
||||
<FunctionOfTheDay handleClose={() => setMode("browse")} />
|
||||
|
||||
{showFunctionOfTheDay ? (
|
||||
<FunctionOfTheDay
|
||||
data={data}
|
||||
handleClose={() => {
|
||||
setMode("browse");
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<List aria-label="basic-list" sx={{ pt: 0 }}>
|
||||
{items.length ? (
|
||||
@ -134,16 +110,20 @@ export function BasicList(props: BasicListProps) {
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
|
||||
{Math.ceil(items.length / itemsPerPage) > 0 && !showFunctionExplore && (
|
||||
<Pagination
|
||||
hideNextButton
|
||||
hidePrevButton
|
||||
{!showFunctionOfTheDay && (
|
||||
<TablePagination
|
||||
component={"div"}
|
||||
sx={{ display: "flex", justifyContent: "center", mt: 1, mb: 10 }}
|
||||
count={Math.ceil(items.length / itemsPerPage) || 1}
|
||||
count={items.length}
|
||||
color="primary"
|
||||
page={page}
|
||||
onChange={handlePageChange}
|
||||
page={page - 1}
|
||||
onPageChange={handlePageChange}
|
||||
rowsPerPage={itemsPerPage}
|
||||
labelRowsPerPage={"per Page"}
|
||||
rowsPerPageOptions={[10, 20, 50, 100]}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
showFirstButton={!isMobile}
|
||||
showLastButton={!isMobile}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
11
components/codeHighlight/codeHighlight.module.css
Normal file
11
components/codeHighlight/codeHighlight.module.css
Normal file
@ -0,0 +1,11 @@
|
||||
|
||||
.hljs {
|
||||
padding-left: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
.hljs .pre {
|
||||
width: 100%;
|
||||
}
|
||||
.hljs .nix {
|
||||
overflow-x: scroll;
|
||||
}
|
35
components/codeHighlight/codeHighlight.tsx
Normal file
35
components/codeHighlight/codeHighlight.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { useEffect } from "react";
|
||||
import Highlight, { HighlightProps } from "react-highlight";
|
||||
import styles from "./codeHighlight.module.css";
|
||||
|
||||
interface CodeHighlightProps {
|
||||
theme: "light" | "dark";
|
||||
code: HighlightProps["children"];
|
||||
}
|
||||
|
||||
export const CodeHighlight = (props: CodeHighlightProps) => {
|
||||
const { theme, code } = props;
|
||||
useEffect(() => {
|
||||
if (theme === "dark") {
|
||||
// @ts-ignore - dont check type of css module
|
||||
import("highlight.js/styles/github-dark.css");
|
||||
} else {
|
||||
// @ts-ignore - dont check type of css module
|
||||
import("highlight.js/styles/github.css");
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
"&.MuiBox-root>pre": {
|
||||
width: "100%",
|
||||
marginTop: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Highlight className={`nix ${styles.hljs}`}>{code}</Highlight>
|
||||
</Box>
|
||||
);
|
||||
};
|
1
components/codeHighlight/index.ts
Normal file
1
components/codeHighlight/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export {CodeHighlight} from "./codeHighlight"
|
@ -9,7 +9,7 @@ import {
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useMemo } from "react";
|
||||
import { DocItem, NixType } from "../../types/nix";
|
||||
import { DocItem, NixType } from "../../models/nix";
|
||||
import { Preview } from "../preview/preview";
|
||||
import StarIcon from "@mui/icons-material/Star";
|
||||
import ShareIcon from "@mui/icons-material/Share";
|
||||
@ -31,14 +31,6 @@ export default function FunctionItem(props: FunctionItemProps) {
|
||||
const { name, docItem, selected, handleClose } = props;
|
||||
const { fn_type, category, description, id } = docItem;
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const [favorites, setfavorites] = useLocalStorage<string[]>(
|
||||
"personal-favorite",
|
||||
[]
|
||||
);
|
||||
const isFavorit = useMemo(
|
||||
() => favorites.includes(getKey(docItem)),
|
||||
[docItem, favorites]
|
||||
);
|
||||
const descriptionPreview = useMemo(() => {
|
||||
const getFirstWords = (s: string) => {
|
||||
const indexOfDot = s.indexOf(".");
|
||||
@ -58,25 +50,10 @@ export default function FunctionItem(props: FunctionItemProps) {
|
||||
}, [description]);
|
||||
|
||||
const handleShare = () => {
|
||||
const queries = [];
|
||||
const key = getKey(docItem);
|
||||
if (key) {
|
||||
queries.push(`fn=${key}`);
|
||||
}
|
||||
const handle = `https://noogle.dev/preview?${queries.join("&")}`;
|
||||
const handle = window.location.href;
|
||||
navigator.clipboard.writeText(handle);
|
||||
enqueueSnackbar("link copied to clipboard", { variant: "default" });
|
||||
};
|
||||
const handleFavorit = () => {
|
||||
const key = getKey(docItem);
|
||||
setfavorites((curr) => {
|
||||
if (curr.includes(key)) {
|
||||
return curr.filter((v) => v !== key);
|
||||
} else {
|
||||
return [...curr, key];
|
||||
}
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Paper
|
||||
elevation={0}
|
||||
@ -101,9 +78,6 @@ export default function FunctionItem(props: FunctionItemProps) {
|
||||
<Stack sx={{ width: "100%" }}>
|
||||
{!selected && (
|
||||
<>
|
||||
<Box sx={{ float: "right", position: "absolute", right: 4 }}>
|
||||
{isFavorit && <StarIcon />}
|
||||
</Box>
|
||||
<ListItemText primary={`${id}`} secondary={category} />
|
||||
<ListItemText secondary={descriptionPreview} />
|
||||
<Typography
|
||||
@ -123,11 +97,6 @@ export default function FunctionItem(props: FunctionItemProps) {
|
||||
justifyContent: "end",
|
||||
}}
|
||||
>
|
||||
<Tooltip title={`${isFavorit ? "remove" : "add"} favorite`}>
|
||||
<IconButton onClick={handleFavorit}>
|
||||
{isFavorit ? <StarIcon /> : <StarBorderIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Share">
|
||||
<IconButton onClick={handleShare}>
|
||||
<ShareIcon />
|
||||
|
@ -11,10 +11,9 @@ import {
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import seedrandom from "seedrandom";
|
||||
import { data } from "../../models/data";
|
||||
import { DocItem } from "../../types/nix";
|
||||
import { Preview } from "../preview/preview";
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
import { DocItem, MetaData } from "../../models/nix";
|
||||
|
||||
const date = new Date();
|
||||
|
||||
@ -36,18 +35,21 @@ function getRandomIntInclusive(
|
||||
return Math.floor(generator() * (max - min + 1) + min); // The maximum is inclusive and the minimum is inclusive
|
||||
}
|
||||
|
||||
const todaysIdx = getRandomIntInclusive(0, data.length - 1, rng);
|
||||
|
||||
interface FunctionOfTheDayProps {
|
||||
handleClose: () => void;
|
||||
data: MetaData;
|
||||
}
|
||||
export const FunctionOfTheDay = (props: FunctionOfTheDayProps) => {
|
||||
const { handleClose } = props;
|
||||
const { handleClose, data } = props;
|
||||
const {
|
||||
palette: { info, error },
|
||||
} = useTheme();
|
||||
const todaysIdx = useMemo(
|
||||
() => getRandomIntInclusive(0, data.length - 1, rng),
|
||||
[data.length]
|
||||
);
|
||||
const [idx, setIdx] = useState<number>(todaysIdx);
|
||||
const slectedFunction = useMemo(() => data.at(idx) as DocItem, [idx]);
|
||||
const selectedFunction = useMemo(() => data.at(idx) as DocItem, [idx, data]);
|
||||
|
||||
const setNext = () => {
|
||||
setIdx((curr) => {
|
||||
@ -101,7 +103,7 @@ export const FunctionOfTheDay = (props: FunctionOfTheDayProps) => {
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
<Preview docItem={slectedFunction} closeComponent={<></>} />
|
||||
<Preview docItem={selectedFunction} closeComponent={<></>} />
|
||||
</CardContent>
|
||||
<CardActions
|
||||
sx={{
|
||||
|
@ -3,9 +3,10 @@ import {
|
||||
Typography,
|
||||
Container,
|
||||
Link,
|
||||
useTheme,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { Image } from "../image";
|
||||
import nixSnowflake from "../../public/nix-snowflake.svg";
|
||||
@ -15,6 +16,8 @@ export interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const useMobile = () => useMediaQuery(useTheme().breakpoints.down("md"));
|
||||
|
||||
export function Layout(props: LayoutProps) {
|
||||
const { children } = props;
|
||||
const theme = useTheme();
|
||||
|
22
components/markdownPreview/MarkdownPreview.tsx
Normal file
22
components/markdownPreview/MarkdownPreview.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import rehypeHighlight from "rehype-highlight";
|
||||
import nix from "highlight.js/lib/languages/nix";
|
||||
|
||||
interface MarkdownPreviewProps {
|
||||
description: string;
|
||||
}
|
||||
export const MarkdownPreview = (props: MarkdownPreviewProps) => {
|
||||
const { description } = props;
|
||||
return (
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
h1: "h3",
|
||||
h2: "h4",
|
||||
h3: "h5",
|
||||
}}
|
||||
rehypePlugins={[[rehypeHighlight, { languages: { nix } }]]}
|
||||
>
|
||||
{description}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
};
|
1
components/markdownPreview/index.ts
Normal file
1
components/markdownPreview/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export {MarkdownPreview} from "./MarkdownPreview"
|
3
components/pageContext/index.ts
Normal file
3
components/pageContext/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { PageContextProvider, PageContext, usePageContext} from "./pageContext";
|
||||
|
||||
export type { SetPageStateVariable } from "./pageContext";
|
77
components/pageContext/pageContext.tsx
Normal file
77
components/pageContext/pageContext.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
InitialPageState,
|
||||
initialPageState,
|
||||
PageState,
|
||||
} from "../../models/internals";
|
||||
|
||||
type PageContextType = {
|
||||
pageState: PageState;
|
||||
setPageState: React.Dispatch<React.SetStateAction<PageState>>;
|
||||
setPageStateVariable: SetPageStateVariable;
|
||||
resetQueries: () => void;
|
||||
};
|
||||
|
||||
export const PageContext = React.createContext<PageContextType>({
|
||||
pageState: { ...initialPageState, FOTD: true },
|
||||
setPageState: () => {},
|
||||
resetQueries: () => {},
|
||||
setPageStateVariable: function a<T>() {
|
||||
return () => {};
|
||||
},
|
||||
});
|
||||
|
||||
interface PageContextProviderProps {
|
||||
children: React.ReactNode;
|
||||
pageProps: PageState;
|
||||
}
|
||||
|
||||
export type SetPageStateVariable = <T>(
|
||||
field: keyof InitialPageState
|
||||
) => (value: React.SetStateAction<T> | T) => void;
|
||||
|
||||
export const PageContextProvider = (props: PageContextProviderProps) => {
|
||||
const router = useRouter();
|
||||
const { children, pageProps } = props;
|
||||
const [pageState, setPageState] = useState<PageState>(pageProps);
|
||||
function setPageStateVariable<T>(field: keyof InitialPageState) {
|
||||
return function (value: React.SetStateAction<T> | T) {
|
||||
{
|
||||
if (typeof value !== "function") {
|
||||
setPageState((curr) => {
|
||||
const query = router.query;
|
||||
query[field] = JSON.stringify(value);
|
||||
router.push({ query });
|
||||
return { ...curr, [field]: value };
|
||||
});
|
||||
} else {
|
||||
const setter = value as Function;
|
||||
setPageState((curr) => {
|
||||
const query = router.query;
|
||||
const newValue = setter(curr[field]);
|
||||
query[field] = JSON.stringify(newValue);
|
||||
router.push({ query });
|
||||
|
||||
return { ...curr, [field]: newValue };
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
function resetQueries() {
|
||||
if (Object.entries(router.query).length !== 0) {
|
||||
router.push({ query: undefined });
|
||||
}
|
||||
setPageState((curr) => ({ ...curr, ...initialPageState }));
|
||||
}
|
||||
return (
|
||||
<PageContext.Provider
|
||||
value={{ pageState, setPageState, setPageStateVariable, resetQueries }}
|
||||
>
|
||||
{children}
|
||||
</PageContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const usePageContext = () => React.useContext(PageContext);
|
1
components/preview/index.tsx
Normal file
1
components/preview/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { Preview } from "./preview";
|
@ -1,11 +0,0 @@
|
||||
|
||||
.hljs {
|
||||
padding-left: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
.hljs .pre {
|
||||
width: 100%;
|
||||
}
|
||||
.hljs .nix {
|
||||
overflow-x: scroll;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React from "react";
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
@ -12,19 +12,13 @@ import {
|
||||
useTheme,
|
||||
Link as MuiLink,
|
||||
} from "@mui/material";
|
||||
import Highlight from "react-highlight";
|
||||
import { DocItem } from "../../models/nix";
|
||||
import CodeIcon from "@mui/icons-material/Code";
|
||||
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
||||
import LocalLibraryIcon from "@mui/icons-material/LocalLibrary";
|
||||
import InputIcon from "@mui/icons-material/Input";
|
||||
import { DocItem } from "../../types/nix";
|
||||
import CodeIcon from "@mui/icons-material/Code";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
||||
import styles from "./preview.module.css";
|
||||
import rehypeHighlight from "rehype-highlight";
|
||||
import nix from "highlight.js/lib/languages/nix";
|
||||
import Link from "next/link";
|
||||
import { idText } from "typescript";
|
||||
import { TrendingUpSharp } from "@mui/icons-material";
|
||||
import { MarkdownPreview } from "../markdownPreview";
|
||||
import { CodeHighlight } from "../codeHighlight";
|
||||
|
||||
interface PreviewProps {
|
||||
docItem: DocItem;
|
||||
@ -37,16 +31,6 @@ export const Preview = (props: PreviewProps) => {
|
||||
const { name, description, category, example, fn_type, id } = docItem;
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (theme.palette.mode === "dark") {
|
||||
// @ts-ignore - dont check type of css module
|
||||
import("highlight.js/styles/github-dark.css");
|
||||
} else {
|
||||
// @ts-ignore - dont check type of css module
|
||||
import("highlight.js/styles/github.css");
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
const prefix = category.split(/([\/.])/gm).at(4) || "builtins";
|
||||
const libName = category
|
||||
.match(/(?:[a-zA-Z]*)\.nix/gm)?.[0]
|
||||
@ -155,33 +139,10 @@ export const Preview = (props: PreviewProps) => {
|
||||
>
|
||||
{Array.isArray(description)
|
||||
? description.map((d, idx) => (
|
||||
<ReactMarkdown
|
||||
key={idx}
|
||||
components={{
|
||||
h1: "h3",
|
||||
h2: "h4",
|
||||
h3: "h5",
|
||||
}}
|
||||
rehypePlugins={[
|
||||
[rehypeHighlight, { languages: { nix } }],
|
||||
]}
|
||||
>
|
||||
{d}
|
||||
</ReactMarkdown>
|
||||
<MarkdownPreview key={idx} description={d} />
|
||||
))
|
||||
: description && (
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
h1: "h3",
|
||||
h2: "h4",
|
||||
h3: "h5",
|
||||
}}
|
||||
rehypePlugins={[
|
||||
[rehypeHighlight, { languages: { nix } }],
|
||||
]}
|
||||
>
|
||||
{description}
|
||||
</ReactMarkdown>
|
||||
<MarkdownPreview description={description} />
|
||||
)}
|
||||
</Container>
|
||||
}
|
||||
@ -248,17 +209,7 @@ export const Preview = (props: PreviewProps) => {
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<Box
|
||||
sx={{
|
||||
"&.MuiBox-root>pre": {
|
||||
width: "100%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Highlight className={`nix ${styles.hljs}`}>
|
||||
{example}
|
||||
</Highlight>
|
||||
</Box>
|
||||
<CodeHighlight code={example} theme={theme.palette.mode} />
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
|
@ -1,56 +0,0 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import * as React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { SearchInput } from "./";
|
||||
|
||||
describe("render search", () => {
|
||||
const mockedSearch = jest.fn();
|
||||
mockedSearch.mockImplementation((term: string) => {
|
||||
return term;
|
||||
});
|
||||
const mockedClear = jest.fn();
|
||||
mockedClear.mockImplementation(() => {
|
||||
return undefined;
|
||||
});
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<SearchInput
|
||||
handleSearch={mockedSearch}
|
||||
clearSearch={mockedClear}
|
||||
placeholder={"Search"}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
test("renders all elements from the searchbar", async () => {
|
||||
const inputBar = screen.getByRole("textbox");
|
||||
const clearButton = screen.getByRole("button", { name: "clear-button" });
|
||||
const searchButton = screen.getByRole("button", { name: "search-button" });
|
||||
expect(inputBar).toBeVisible();
|
||||
expect(searchButton).toBeVisible();
|
||||
expect(clearButton).toBeVisible();
|
||||
const term = "search for term";
|
||||
await userEvent.type(inputBar, term);
|
||||
await userEvent.click(searchButton);
|
||||
expect(mockedSearch).toBeCalledWith(term);
|
||||
});
|
||||
|
||||
test("search for a term", async () => {
|
||||
const inputBar = screen.getByRole("textbox");
|
||||
const searchButton = screen.getByRole("button", { name: "search-button" });
|
||||
const term = "search for term";
|
||||
await userEvent.type(inputBar, term);
|
||||
await userEvent.click(searchButton);
|
||||
expect(mockedSearch).toBeCalledWith(term);
|
||||
});
|
||||
|
||||
test("clearing search", async () => {
|
||||
const inputBar = screen.getByRole("textbox");
|
||||
const clearButton = screen.getByRole("button", { name: "clear-button" });
|
||||
const term = "search for term";
|
||||
await userEvent.type(inputBar, term);
|
||||
await userEvent.click(clearButton);
|
||||
expect(mockedClear).toBeCalled();
|
||||
});
|
||||
});
|
@ -1,171 +1,50 @@
|
||||
import React, { useState, useMemo, useEffect } from "react";
|
||||
import React, { useState, useMemo } from "react";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import InputBase from "@mui/material/InputBase";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
import {
|
||||
Box,
|
||||
debounce,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
FormLabel,
|
||||
Grid,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Box, debounce, Grid, Tooltip, Typography } from "@mui/material";
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||
import { NixType, nixTypes } from "../../types/nix";
|
||||
import { useRouter } from "next/router";
|
||||
import ShareIcon from "@mui/icons-material/Share";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
interface SelectOptionProps {
|
||||
label: string;
|
||||
handleChange: (value: string) => void;
|
||||
value: string;
|
||||
|
||||
options: {
|
||||
value: string;
|
||||
label: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const SelectOption = (props: SelectOptionProps) => {
|
||||
const { label, handleChange, options, value } = props;
|
||||
|
||||
const _handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newVal = (event.target as HTMLInputElement).value as NixType;
|
||||
handleChange(newVal);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
sx={{
|
||||
flexDirection: "row",
|
||||
}}
|
||||
>
|
||||
<FormLabel
|
||||
sx={{
|
||||
width: "7rem",
|
||||
wordWrap: "unset",
|
||||
margin: "auto",
|
||||
padding: 1,
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ minWidth: "max-content" }}>{label}</Typography>
|
||||
</FormLabel>
|
||||
|
||||
<RadioGroup
|
||||
sx={{
|
||||
width: "100%",
|
||||
"&.MuiFormGroup-root": {
|
||||
flexDirection: "row",
|
||||
},
|
||||
}}
|
||||
value={value}
|
||||
onChange={_handleChange}
|
||||
>
|
||||
{options.map(({ value, label }) => (
|
||||
<FormControlLabel
|
||||
key={value}
|
||||
value={value}
|
||||
control={<Radio />}
|
||||
label={label}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
import { NixType, nixTypes } from "../../models/nix";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import { usePageContext } from "../pageContext";
|
||||
import { initialPageState } from "../../models/internals";
|
||||
import { SelectOption } from "../selectOption";
|
||||
|
||||
export type Filter = { from: NixType; to: NixType };
|
||||
|
||||
export interface SearchInputProps {
|
||||
handleSearch: (term: string) => void;
|
||||
handleClear: () => void;
|
||||
handleFilter: (filter: Filter | ((curr: Filter) => Filter)) => void;
|
||||
clearSearch: () => void;
|
||||
placeholder: string;
|
||||
page: number;
|
||||
}
|
||||
|
||||
export function SearchInput(props: SearchInputProps) {
|
||||
const { handleSearch, clearSearch, placeholder, handleFilter, page } = props;
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const [term, setTerm] = useState("");
|
||||
const [to, setTo] = useState<NixType>("any");
|
||||
const [from, setFrom] = useState<NixType>("any");
|
||||
const { handleSearch, placeholder, handleFilter, handleClear } = props;
|
||||
const { pageState } = usePageContext();
|
||||
const { filter, term } = pageState;
|
||||
const [_term, _setTerm] = useState(term);
|
||||
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
const { query } = router;
|
||||
if (query?.search) {
|
||||
if (typeof query.search === "string") {
|
||||
const search = query.search;
|
||||
setTerm(search);
|
||||
handleSearch(search);
|
||||
}
|
||||
}
|
||||
if (query?.to) {
|
||||
if (typeof query.to === "string") {
|
||||
const to = query.to as NixType;
|
||||
setTo(to);
|
||||
handleFilter((curr) => ({ ...curr, to }));
|
||||
}
|
||||
}
|
||||
if (query?.from) {
|
||||
if (typeof query.from === "string") {
|
||||
const from = query.from as NixType;
|
||||
setFrom(from);
|
||||
handleFilter((curr) => ({ ...curr, from }));
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router]);
|
||||
|
||||
const handleShare = () => {
|
||||
const queries = [];
|
||||
if (term) {
|
||||
queries.push(`search=${term}`);
|
||||
}
|
||||
|
||||
queries.push(`page=${page}`);
|
||||
|
||||
queries.push(`to=${to}&from=${from}`);
|
||||
const handle = `https://noogle.dev?${queries.join("&")}`;
|
||||
navigator.clipboard.writeText(handle);
|
||||
enqueueSnackbar("link copied to clipboard", { variant: "default" });
|
||||
};
|
||||
const handleSubmit = React.useRef((input: string) => {
|
||||
handleSearch(input);
|
||||
router.query[term] = term;
|
||||
}).current;
|
||||
|
||||
const debouncedSubmit = useMemo(
|
||||
() => debounce(handleSubmit, 300),
|
||||
() => debounce(handleSubmit, 500),
|
||||
[handleSubmit]
|
||||
);
|
||||
|
||||
const _handleFilter = (t: NixType, mode: "from" | "to") => {
|
||||
if (mode === "to") {
|
||||
setTo(t);
|
||||
handleFilter({ to: t, from });
|
||||
} else {
|
||||
setFrom(t);
|
||||
handleFilter({ to, from: t });
|
||||
}
|
||||
const _handleClear = () => {
|
||||
_setTerm("");
|
||||
handleClear();
|
||||
// handleFilter(initialPageState.filter);
|
||||
// handleSubmit("");
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setTerm("");
|
||||
setFrom("any");
|
||||
setTo("any");
|
||||
clearSearch();
|
||||
};
|
||||
const handleChange = (
|
||||
const handleType = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
setTerm(e.target.value);
|
||||
_setTerm(e.target.value);
|
||||
debouncedSubmit(e.target.value);
|
||||
};
|
||||
|
||||
@ -186,7 +65,7 @@ export function SearchInput(props: SearchInputProps) {
|
||||
handleSubmit(term);
|
||||
}}
|
||||
>
|
||||
<IconButton aria-label="clear-button" onClick={handleClear}>
|
||||
<IconButton aria-label="clear-button" onClick={_handleClear}>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
<InputBase
|
||||
@ -199,18 +78,17 @@ export function SearchInput(props: SearchInputProps) {
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
inputProps={{ "aria-label": "search-input" }}
|
||||
value={term}
|
||||
onChange={(e) => handleChange(e)}
|
||||
value={_term}
|
||||
onChange={(e) => handleType(e)}
|
||||
/>
|
||||
<Tooltip title="share search result">
|
||||
<IconButton
|
||||
sx={{
|
||||
p: 1,
|
||||
color: "common.black",
|
||||
}}
|
||||
onClick={handleShare}
|
||||
aria-label="search-button"
|
||||
>
|
||||
<ShareIcon fontSize="inherit" />
|
||||
<SearchIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Paper>
|
||||
@ -219,10 +97,10 @@ export function SearchInput(props: SearchInputProps) {
|
||||
<Grid container>
|
||||
<Grid item xs={12} md={5}>
|
||||
<SelectOption
|
||||
value={from}
|
||||
value={filter.from}
|
||||
label="from type"
|
||||
handleChange={(value) => {
|
||||
_handleFilter(value as NixType, "from");
|
||||
handleFilter((curr) => ({ ...curr, from: value as NixType }));
|
||||
}}
|
||||
options={nixTypes.map((v) => ({ value: v, label: v }))}
|
||||
/>
|
||||
@ -245,10 +123,10 @@ export function SearchInput(props: SearchInputProps) {
|
||||
</Grid>
|
||||
<Grid item xs={12} md={5}>
|
||||
<SelectOption
|
||||
value={to}
|
||||
value={filter.to}
|
||||
label="to type"
|
||||
handleChange={(value) => {
|
||||
_handleFilter(value as NixType, "to");
|
||||
handleFilter((curr) => ({ ...curr, to: value as NixType }));
|
||||
}}
|
||||
options={nixTypes.map((v) => ({ value: v, label: v }))}
|
||||
/>
|
||||
|
1
components/selectOption/index.ts
Normal file
1
components/selectOption/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export {SelectOption} from "./selectOption"
|
68
components/selectOption/selectOption.tsx
Normal file
68
components/selectOption/selectOption.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
FormLabel,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { NixType } from "../../models/nix";
|
||||
|
||||
interface SelectOptionProps {
|
||||
label: string;
|
||||
handleChange: (value: string) => void;
|
||||
value: string;
|
||||
|
||||
options: {
|
||||
value: string;
|
||||
label: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const SelectOption = (props: SelectOptionProps) => {
|
||||
const { label, handleChange, options, value } = props;
|
||||
|
||||
const _handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newVal = (event.target as HTMLInputElement).value as NixType;
|
||||
handleChange(newVal);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
sx={{
|
||||
flexDirection: "row",
|
||||
}}
|
||||
>
|
||||
<FormLabel
|
||||
sx={{
|
||||
width: "7rem",
|
||||
wordWrap: "unset",
|
||||
margin: "auto",
|
||||
padding: 1,
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ minWidth: "max-content" }}>{label}</Typography>
|
||||
</FormLabel>
|
||||
|
||||
<RadioGroup
|
||||
sx={{
|
||||
width: "100%",
|
||||
"&.MuiFormGroup-root": {
|
||||
flexDirection: "row",
|
||||
},
|
||||
}}
|
||||
value={value}
|
||||
onChange={_handleChange}
|
||||
>
|
||||
{options.map(({ value, label }) => (
|
||||
<FormControlLabel
|
||||
key={value}
|
||||
value={value}
|
||||
control={<Radio />}
|
||||
label={label}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
import { MetaData } from "../types/nix";
|
||||
import nixLibs from "../models/lib.json";
|
||||
import nixBuiltins from "../models/builtins.json";
|
||||
import nixTrivialBuilders from "../models/trivial-builders.json";
|
||||
|
||||
export const data: MetaData = [
|
||||
...(nixLibs as MetaData),
|
||||
...(nixBuiltins as MetaData),
|
||||
...(nixTrivialBuilders as MetaData),
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
10
models/data/index.ts
Normal file
10
models/data/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { MetaData } from "../nix";
|
||||
import nixLibs from "./lib.json";
|
||||
import nixBuiltins from "./builtins.json";
|
||||
import nixTrivialBuilders from "./trivial-builders.json";
|
||||
|
||||
export const data: MetaData = [
|
||||
...(nixLibs as MetaData),
|
||||
...(nixBuiltins as MetaData),
|
||||
...(nixTrivialBuilders as MetaData),
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
28
models/internals.ts
Normal file
28
models/internals.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { data } from "./data";
|
||||
import { MetaData, NixType } from "./nix";
|
||||
|
||||
export type ComputedState = {
|
||||
FOTD: boolean;
|
||||
}
|
||||
|
||||
export type PageState = {
|
||||
data: MetaData;
|
||||
selected: string | null;
|
||||
term: string;
|
||||
filter: Filter;
|
||||
page: number;
|
||||
itemsPerPage: number;
|
||||
} & ComputedState;
|
||||
|
||||
export type InitialPageState = Omit<PageState, keyof ComputedState>;
|
||||
|
||||
export const initialPageState: InitialPageState = {
|
||||
data,
|
||||
selected: null,
|
||||
term: "",
|
||||
filter: { from: "any", to: "any" },
|
||||
page: 1,
|
||||
itemsPerPage: 10,
|
||||
};
|
||||
|
||||
export type Filter = { to: NixType; from: NixType };
|
@ -1,4 +1,5 @@
|
||||
export type NixType = "function" | "attrset" | "list" | "string" | "int" | "bool" | "any";
|
||||
|
||||
export const nixTypes: NixType[] = [
|
||||
"any",
|
||||
"attrset",
|
@ -36,11 +36,18 @@ const MyApp: React.FunctionComponent<MyAppProps> = (props) => {
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>noogle</title>
|
||||
<meta name="description" content="Search nix functions" />
|
||||
<meta charSet="utf-8" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Search nix functions. Search functions within the nix ecosystem based on type, name, description, example, category and more."
|
||||
/>
|
||||
<meta />
|
||||
<meta name="robots" content="all" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
</Head>
|
||||
|
||||
|
145
pages/index.tsx
145
pages/index.tsx
@ -1,80 +1,77 @@
|
||||
import { BasicList, BasicListItem } from "../components/basicList";
|
||||
import { useState, useMemo } from "react";
|
||||
import { Box } from "@mui/material";
|
||||
import FunctionItem from "../components/functionItem/functionItem";
|
||||
import { NixType, nixTypes, MetaData, DocItem } from "../types/nix";
|
||||
import { data } from "../models/data";
|
||||
import { byQuery, byType, pipe } from "../queries";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
initialPageState,
|
||||
PageState,
|
||||
InitialPageState,
|
||||
} from "../models/internals";
|
||||
import { PageContext, PageContextProvider } from "../components/pageContext";
|
||||
import NixFunctions from "../components/NixFunctions";
|
||||
|
||||
import { NextRouter, useRouter } from "next/router";
|
||||
import { LinearProgress } from "@mui/material";
|
||||
|
||||
const getInitialProps = async (context: NextRouter) => {
|
||||
const { query } = context;
|
||||
const initialProps = { ...initialPageState };
|
||||
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
if (value && !Array.isArray(value)) {
|
||||
try {
|
||||
const parsedValue = JSON.parse(value) as never;
|
||||
const initialValue = initialPageState[key as keyof InitialPageState];
|
||||
|
||||
if (!initialValue || typeof parsedValue === typeof initialValue) {
|
||||
initialProps[key as keyof InitialPageState] = JSON.parse(
|
||||
value
|
||||
) as never;
|
||||
} else {
|
||||
throw "Type of query param does not match the initial values type";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Invalid query:", { key, value, error });
|
||||
}
|
||||
}
|
||||
});
|
||||
const FOTD = Object.entries(query).length === 0;
|
||||
|
||||
return {
|
||||
props: {
|
||||
...initialProps,
|
||||
FOTD,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default function FunctionsPage() {
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [term, setTerm] = useState<string>("");
|
||||
const [filter, setFilter] = useState<{ to: NixType; from: NixType }>({
|
||||
to: "any",
|
||||
from: "any",
|
||||
});
|
||||
|
||||
const handleSelect = (key: string) => {
|
||||
console.log({ key });
|
||||
setSelected((curr: string | null) => {
|
||||
if (curr === key) {
|
||||
return null;
|
||||
} else {
|
||||
return key;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const filteredData = useMemo(
|
||||
() => pipe(byType(filter), byQuery(term))(data),
|
||||
[filter, term]
|
||||
);
|
||||
|
||||
const handleSearch = (term: string) => {
|
||||
setTerm(term);
|
||||
};
|
||||
|
||||
type Filter = { from: NixType; to: NixType };
|
||||
const handleFilter = (newFilter: Filter | ((curr: Filter) => Filter)) => {
|
||||
setFilter(newFilter);
|
||||
};
|
||||
const getKey = (item: DocItem) => `${item.category}/${item.name}`;
|
||||
|
||||
const preRenderedItems: BasicListItem[] = filteredData.map(
|
||||
(docItem: DocItem) => {
|
||||
const key = getKey(docItem);
|
||||
return {
|
||||
item: (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
onClick={!(selected === key) ? () => handleSelect(key) : undefined}
|
||||
>
|
||||
<FunctionItem
|
||||
name={docItem.name}
|
||||
docItem={docItem}
|
||||
selected={selected === key}
|
||||
handleClose={() => setSelected(null)}
|
||||
/>
|
||||
</Box>
|
||||
),
|
||||
key,
|
||||
};
|
||||
const router = useRouter();
|
||||
const [initialProps, setInitialProps] = useState<PageState | null>(null);
|
||||
useEffect(() => {
|
||||
if (router.isReady) {
|
||||
getInitialProps(router).then((r) => {
|
||||
const { props } = r;
|
||||
console.info("Url Query changed\n\nUpdating pageState with delta:", {
|
||||
props,
|
||||
});
|
||||
setInitialProps((curr) => ({ ...curr, ...props }));
|
||||
});
|
||||
}
|
||||
);
|
||||
}, [router]);
|
||||
return (
|
||||
<Box sx={{ ml: { xs: 0, md: 2 }, mb: 5 }}>
|
||||
<BasicList
|
||||
term={term}
|
||||
filter={filter}
|
||||
selected={selected}
|
||||
itemsPerPage={8}
|
||||
items={preRenderedItems}
|
||||
handleSearch={handleSearch}
|
||||
handleFilter={handleFilter}
|
||||
/>
|
||||
</Box>
|
||||
<>
|
||||
{!initialProps ? (
|
||||
<LinearProgress />
|
||||
) : (
|
||||
<PageContextProvider pageProps={initialProps}>
|
||||
<PageContext.Consumer>
|
||||
{(context) => (
|
||||
<NixFunctions
|
||||
pageState={context.pageState}
|
||||
setPageStateVariable={context.setPageStateVariable}
|
||||
/>
|
||||
)}
|
||||
</PageContext.Consumer>
|
||||
</PageContextProvider>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,43 +0,0 @@
|
||||
import { IconButton, Paper, Tooltip } from "@mui/material";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Preview } from "../components/preview/preview";
|
||||
import { data } from "../models/data";
|
||||
import { DocItem } from "../types/nix";
|
||||
import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos";
|
||||
|
||||
const getKey = (item: DocItem) => `${item.category}/${item.name}`;
|
||||
|
||||
export default function PreviewPage() {
|
||||
const router = useRouter();
|
||||
const [item, setItem] = useState<DocItem | null>(null);
|
||||
useEffect(() => {
|
||||
const { query } = router;
|
||||
if (query?.fn) {
|
||||
if (typeof query.fn === "string") {
|
||||
const name = query.fn;
|
||||
setItem(data.find((d) => getKey(d) === name) || null);
|
||||
}
|
||||
}
|
||||
}, [router]);
|
||||
return (
|
||||
item && (
|
||||
<>
|
||||
<Paper sx={{ py: 4 }} elevation={0}>
|
||||
<Tooltip title="back to list">
|
||||
<IconButton
|
||||
sx={{
|
||||
mx: 3,
|
||||
}}
|
||||
size="small"
|
||||
onClick={() => router.push("/")}
|
||||
>
|
||||
<ArrowBackIosIcon fontSize="large" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Preview docItem={item} closeComponent={<span></span>} />
|
||||
</Paper>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { DocItem, MetaData } from "../types/nix";
|
||||
import { DocItem, MetaData } from "../models/nix";
|
||||
|
||||
export const byQuery =
|
||||
(rawTerm: string) =>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { MetaData, NixType } from "../types/nix";
|
||||
import { MetaData, NixType } from "../models/nix";
|
||||
import { getTypes } from "./lib";
|
||||
|
||||
export const byType =
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { NixType, nixTypes } from "../types/nix";
|
||||
import { NixType, nixTypes } from "../models/nix";
|
||||
|
||||
export function pipe<T>(...fns: ((arr: T) => T)[]) {
|
||||
return (x: T) => fns.reduce((v, f) => f(v), x);
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "es6",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
Loading…
Reference in New Issue
Block a user