INIT: first prototype of frontend

This commit is contained in:
Johannes Kirschbauer 2022-11-26 10:36:08 +01:00
commit c51722be2d
34 changed files with 7663 additions and 0 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

3
.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

44
.gitignore vendored Normal file
View File

@ -0,0 +1,44 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# nix
.direnv/
result
result-*
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
result
result-*
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

27
README.md Normal file
View File

@ -0,0 +1,27 @@
# Static page with nextjs example
- Generates a static page
- Includes custom Image loader for static page generation
## Build
> ### ! The build step is completly independent of develop step and can be run right after checkout
`nix build .#`
This should create to following result:
- static/
- __index.html__
- favicon.ico
- 404.html
- vercel-svg
- _next/
- static/...
- ...
## Develop
`nix develop`
This command creates the node_modules folder with all needed dependencies

View File

@ -0,0 +1,103 @@
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();
});
});

View File

@ -0,0 +1,175 @@
import React, { useState } from "react";
import {
Box,
IconButton,
List,
ListItem,
Pagination,
Stack,
Typography,
Grid,
} from "@mui/material";
import { BasicDataViewProps, NixType } 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 ChevronRightIcon from "@mui/icons-material/ChevronRight";
export type BasicListItem = {
item: React.ReactNode;
key: string;
};
export type BasicListProps = BasicDataViewProps & {
handleFilter: (t: NixType, mode: "from" | "to") => void;
preview: React.ReactNode;
};
interface SelectOptionProps {
label: string;
handleChange: (value: string) => void;
options: {
value: string;
label: string;
}[];
}
const SelectOption = (props: SelectOptionProps) => {
const { label, handleChange, options } = props;
const [value, setValue] = React.useState<NixType>("any");
const _handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newVal = (event.target as HTMLInputElement).value as NixType;
setValue(newVal);
handleChange(newVal);
};
const handleClear = () => {
setValue("any");
handleChange("any");
};
return (
<FormControl>
<FormLabel>
<Box>
<IconButton
// disabled={value === ""}
aria-label="clear-button"
onClick={handleClear}
>
<ClearIcon />
</IconButton>
{label}
</Box>
</FormLabel>
<RadioGroup sx={{ pl: 1.5 }} value={value} onChange={_handleChange}>
{options.map(({ value, label }) => (
<FormControlLabel
key={value}
value={value}
control={<Radio />}
label={label}
/>
))}
</RadioGroup>
</FormControl>
);
};
export function BasicList(props: BasicListProps) {
const { items, pageCount = 1, handleSearch, handleFilter, preview } = props;
// const [from, setFrom] = useState<NixType>("any");
// const [to, setTo] = useState<NixType>("any");
const [page, setPage] = useState(1);
const [searchTerm, setSearchTerm] = useState("");
const handlePageChange = (
_event: React.ChangeEvent<unknown>,
value: number
) => {
setPage(value);
};
const _handleFilter = (t: NixType, mode: "from" | "to") => {
handleFilter(t, mode);
setPage(1);
};
const _handleSearch = (term: string) => {
handleSearch && handleSearch(term);
setSearchTerm(term);
setPage(1);
};
const nixTypes: NixType[] = [
"any",
"attrset",
"list",
"string",
"bool",
"int",
];
return (
<Stack>
<SearchInput
placeholder="search nix functions"
handleSearch={_handleSearch}
clearSearch={() => _handleSearch("")}
/>
<Box>
<Grid container>
<Grid item xs={3}>
<Stack direction="row">
<SelectOption
label="from type"
handleChange={(value) => {
_handleFilter(value as NixType, "from");
}}
options={nixTypes.map((v) => ({ value: v, label: v }))}
/>
<Typography
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
}}
>
<ChevronRightIcon />
</Typography>
<SelectOption
label="to type"
handleChange={(value) => {
_handleFilter(value as NixType, "to");
}}
options={nixTypes.map((v) => ({ value: v, label: v }))}
/>
</Stack>
</Grid>
<Grid item xs={9}>
{preview}
</Grid>
</Grid>
</Box>
<List aria-label="basic-list" sx={{ pt: 0 }}>
{items.map(({ item, key }) => (
<ListItem key={key} aria-label={`item-${key}`} sx={{ px: 0 }}>
{item}
</ListItem>
))}
</List>
<Pagination
sx={{ display: "flex", justifyContent: "center", mt: 1, mb: 10 }}
count={pageCount}
color="primary"
page={page}
onChange={handlePageChange}
/>
</Stack>
);
}

View File

@ -0,0 +1,2 @@
export { BasicList } from "./basicList";
export type { BasicListProps, BasicListItem } from "./basicList";

View File

@ -0,0 +1,40 @@
import {
Box,
Card,
ListItemText,
Paper,
Stack,
Typography,
} from "@mui/material";
interface FunctionItemProps {
selected: boolean;
name: String;
info: {
"attr-path": String;
source: String;
from: String;
to: String;
};
}
export default function FunctionItem(props: FunctionItemProps) {
const { name, info, selected } = props;
return (
<Paper
elevation={0}
sx={{
display: "flex",
justifyContent: "left",
color: selected ? "primary.main" : undefined,
borderColor: selected ? "action.selected" : "none",
borderWidth: 1,
borderStyle: selected ? "solid" : "none",
}}
>
<Stack>
<ListItemText primary={name} secondary={info["attr-path"]} />
<Typography>{`${info.from} -> ${info.to} `}</Typography>
</Stack>
</Paper>
);
}

View File

@ -0,0 +1,10 @@
import NextImage, { ImageLoaderProps, ImageProps } from "next/image";
// opt-out of image optimization, no-op
const customLoader = ({ src }: ImageLoaderProps) => {
return src;
};
export function Image(props: ImageProps) {
return <NextImage {...props} loader={customLoader} />;
}

View File

@ -0,0 +1 @@
export { Image } from "./image";

View File

@ -0,0 +1 @@
export { Layout } from "./layout"

View File

@ -0,0 +1,42 @@
import { Box, Typography, Container, Link } from "@mui/material";
// import Link from "next/link";
export interface LayoutProps {
children: React.ReactNode;
}
export function Layout(props: LayoutProps) {
const { children } = props;
return (
<>
<header>
<Box sx={{ p: 2, backgroundColor: "primary.main" }}>
<Typography variant="h1" sx={{ textAlign: "center", color: "#fff" }}>
{`noog\u03BBe`}
</Typography>
</Box>
</header>
<main>
<Container sx={{ pt: 0 }} maxWidth="xl">
{children}
</Container>
</main>
<footer
style={{
position: "fixed",
bottom: 0,
display: "flex",
justifyContent: "center",
width: "100%",
}}
>
Powered by{" "}
<Link sx={{ ml: 1 }} href="https://oceansprint.org/">
{" "}
OceanSprint
</Link>
</footer>
</>
);
}

View File

@ -0,0 +1,55 @@
import {
Box,
Link,
List,
ListItem,
ListItemIcon,
ListItemSecondaryAction,
ListItemText,
Typography,
} from "@mui/material";
import { info } from "console";
import { FuncData } from "../../pages";
import LocalLibraryIcon from "@mui/icons-material/LocalLibrary";
import InputIcon from "@mui/icons-material/Input";
import OutputIcon from "@mui/icons-material/Output";
interface PreviewProps {
func: FuncData;
}
export const Preview = (props: PreviewProps) => {
const { func } = props;
return (
<Box sx={{ p: 1, width: "100%" }}>
<Typography variant="h2">{func.name}</Typography>
<List sx={{ width: "100%" }}>
<ListItem>
<ListItemIcon>
<LocalLibraryIcon />
</ListItemIcon>
<ListItemText
primary={func.info["attr-path"]}
secondary={func.info["doc-url"]}
/>
<ListItemSecondaryAction>
<Link href={func.info["doc-url"]}>View Docs</Link>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemIcon>
<InputIcon />
</ListItemIcon>
<ListItemText primary={func.info.from} secondary="argument type" />
</ListItem>
<ListItem>
<ListItemIcon>
<OutputIcon />
</ListItemIcon>
<ListItemText primary={func.info.to} secondary="return type" />
</ListItem>
<ListItem></ListItem>
</List>
</Box>
);
};

View File

@ -0,0 +1 @@
export { SearchInput } from "./searchInput";

View File

@ -0,0 +1,56 @@
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();
});
});

View File

@ -0,0 +1,84 @@
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 SearchIcon from "@mui/icons-material/Search";
import ClearIcon from "@mui/icons-material/Clear";
import { debounce } from "@mui/material";
export interface SearchInputProps {
handleSearch: (term: string) => void;
clearSearch: () => void;
placeholder: string;
}
export function SearchInput(props: SearchInputProps) {
const { handleSearch, clearSearch, placeholder } = props;
const [term, setTerm] = useState("");
const handleSubmit = React.useRef((input: string) => {
handleSearch(input);
}).current;
const debouncedSubmit = useMemo(
() => debounce(handleSubmit, 300),
[handleSubmit]
);
const handleClear = () => {
setTerm("");
clearSearch();
};
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setTerm(e.target.value);
debouncedSubmit(e.target.value);
};
return (
<Paper
component="form"
elevation={0}
sx={{
width: "100%",
p: 1,
my: 2,
display: "flex",
alignItems: "center",
}}
onSubmit={(e: React.FormEvent) => {
e.preventDefault();
handleSubmit(term);
}}
>
<IconButton aria-label="clear-button" onClick={handleClear}>
<ClearIcon />
</IconButton>
<InputBase
autoFocus
sx={{ ml: 1, flex: 1, backgroundColor: "#fff", p: 1 }}
placeholder={placeholder}
inputProps={{ "aria-label": "search-input" }}
value={term}
onChange={(e) => handleChange(e)}
/>
<IconButton
type="submit"
onClick={() => handleSubmit(term)}
sx={{
p: 1,
bgcolor: "primary.dark",
color: "common.white",
"&:hover": {
backgroundColor: "primary.main",
opacity: [0.9, 0.8, 0.7],
},
}}
aria-label="search-button"
>
<SearchIcon />
</IconButton>
</Paper>
);
}

7
createEmotionCache.ts Normal file
View File

@ -0,0 +1,7 @@
import createCache from "@emotion/cache";
const createEmotionCache = () => {
return createCache({ key: "css", prepend: true });
};
export default createEmotionCache;

286
flake.lock Normal file
View File

@ -0,0 +1,286 @@
{
"nodes": {
"alejandra": {
"inputs": {
"fenix": "fenix",
"flakeCompat": "flakeCompat",
"nixpkgs": [
"dream2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1658427149,
"narHash": "sha256-ToD/1z/q5VHsLMrS2h96vjJoLho59eNRtknOUd19ey8=",
"owner": "kamadorueda",
"repo": "alejandra",
"rev": "f5a22afd2adfb249b4e68e0b33aa1f0fb73fb1be",
"type": "github"
},
"original": {
"owner": "kamadorueda",
"repo": "alejandra",
"type": "github"
}
},
"all-cabal-json": {
"flake": false,
"locked": {
"lastModified": 1665552503,
"narHash": "sha256-r14RmRSwzv5c+bWKUDaze6pXM7nOsiz1H8nvFHJvufc=",
"owner": "nix-community",
"repo": "all-cabal-json",
"rev": "d7c0434eebffb305071404edcf9d5cd99703878e",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "hackage",
"repo": "all-cabal-json",
"type": "github"
}
},
"crane": {
"flake": false,
"locked": {
"lastModified": 1661875961,
"narHash": "sha256-f1h/2c6Teeu1ofAHWzrS8TwBPcnN+EEu+z1sRVmMQTk=",
"owner": "ipetkov",
"repo": "crane",
"rev": "d9f394e4e20e97c2a60c3ad82c2b6ef99be19e24",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"devshell": {
"flake": false,
"locked": {
"lastModified": 1663445644,
"narHash": "sha256-+xVlcK60x7VY1vRJbNUEAHi17ZuoQxAIH4S4iUFUGBA=",
"owner": "numtide",
"repo": "devshell",
"rev": "e3dc3e21594fe07bdb24bdf1c8657acaa4cb8f66",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"dream2nix": {
"inputs": {
"alejandra": "alejandra",
"all-cabal-json": "all-cabal-json",
"crane": "crane",
"devshell": "devshell",
"flake-utils-pre-commit": "flake-utils-pre-commit",
"ghc-utils": "ghc-utils",
"gomod2nix": "gomod2nix",
"mach-nix": "mach-nix",
"nixpkgs": "nixpkgs",
"poetry2nix": "poetry2nix",
"pre-commit-hooks": "pre-commit-hooks"
},
"locked": {
"lastModified": 1667429039,
"narHash": "sha256-Lu6da25JioHzerkLHAHSO9suCQFzJ/XBjkcGCIbasLM=",
"owner": "nix-community",
"repo": "dream2nix",
"rev": "5252794e58eedb02d607fa3187ffead7becc81b0",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "dream2nix",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"dream2nix",
"alejandra",
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1657607339,
"narHash": "sha256-HaqoAwlbVVZH2n4P3jN2FFPMpVuhxDy1poNOR7kzODc=",
"owner": "nix-community",
"repo": "fenix",
"rev": "b814c83d9e6aa5a28d0cf356ecfdafb2505ad37d",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-utils-pre-commit": {
"locked": {
"lastModified": 1644229661,
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flakeCompat": {
"flake": false,
"locked": {
"lastModified": 1650374568,
"narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "b4a34015c698c7793d592d66adbab377907a2be8",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"ghc-utils": {
"flake": false,
"locked": {
"lastModified": 1662774800,
"narHash": "sha256-1Rd2eohGUw/s1tfvkepeYpg8kCEXiIot0RijapUjAkE=",
"ref": "refs/heads/master",
"rev": "bb3a2d3dc52ff0253fb9c2812bd7aa2da03e0fea",
"revCount": 1072,
"type": "git",
"url": "https://gitlab.haskell.org/bgamari/ghc-utils"
},
"original": {
"type": "git",
"url": "https://gitlab.haskell.org/bgamari/ghc-utils"
}
},
"gomod2nix": {
"flake": false,
"locked": {
"lastModified": 1627572165,
"narHash": "sha256-MFpwnkvQpauj799b4QTBJQFEddbD02+Ln5k92QyHOSk=",
"owner": "tweag",
"repo": "gomod2nix",
"rev": "67f22dd738d092c6ba88e420350ada0ed4992ae8",
"type": "github"
},
"original": {
"owner": "tweag",
"repo": "gomod2nix",
"type": "github"
}
},
"mach-nix": {
"flake": false,
"locked": {
"lastModified": 1634711045,
"narHash": "sha256-m5A2Ty88NChLyFhXucECj6+AuiMZPHXNbw+9Kcs7F6Y=",
"owner": "DavHau",
"repo": "mach-nix",
"rev": "4433f74a97b94b596fa6cd9b9c0402104aceef5d",
"type": "github"
},
"original": {
"id": "mach-nix",
"type": "indirect"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1665580254,
"narHash": "sha256-hO61XPkp1Hphl4HGNzj1VvDH5URt7LI6LaY/385Eul4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f634d427b0224a5f531ea5aa10c3960ba6ec5f0f",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"poetry2nix": {
"flake": false,
"locked": {
"lastModified": 1632969109,
"narHash": "sha256-jPDclkkiAy5m2gGLBlKgH+lQtbF7tL4XxBrbSzw+Ioc=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "aee8f04296c39d88155e05d25cfc59dfdd41cc77",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "1.21.0",
"repo": "poetry2nix",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-utils": [
"dream2nix",
"flake-utils-pre-commit"
],
"nixpkgs": [
"dream2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1646153636,
"narHash": "sha256-AlWHMzK+xJ1mG267FdT8dCq/HvLCA6jwmx2ZUy5O8tY=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "b6bc0b21e1617e2b07d8205e7fae7224036dfa4b",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": {
"inputs": {
"dream2nix": "dream2nix"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1657557289,
"narHash": "sha256-PRW+nUwuqNTRAEa83SfX+7g+g8nQ+2MMbasQ9nt6+UM=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "caf23f29144b371035b864a1017dbc32573ad56d",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

30
flake.nix Normal file
View File

@ -0,0 +1,30 @@
{
inputs.dream2nix.url = "github:nix-community/dream2nix";
outputs = inp:
let
inherit (builtins.fromJSON (builtins.readFile ./package.json)) name;
in
inp.dream2nix.lib.makeFlakeOutputs {
systemsFromFile = ./nix_systems;
config.projectRoot = ./.;
source = ./.;
settings = [
{
subsystemInfo.nodejs = 18;
}
];
packageOverrides = {
${name}.staticPage = {
installPhase = ''
runHook preInstall
npm run export
mkdir -p $out/static
cp -r ./out/* $out/static/
runHook postInstall
'';
};
};
};
}

10
next.config.js Normal file
View File

@ -0,0 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
images: {
loader: "custom",
},
};
module.exports = nextConfig;

1
nix_systems Normal file
View File

@ -0,0 +1 @@
x86_64-linux

6059
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "nextapp",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"export": "next export",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@fontsource/roboto": "^4.5.8",
"@mui/icons-material": "^5.10.9",
"@mui/material": "^5.10.13",
"@types/node": "18.11.9",
"@types/react": "^18.0.25",
"@types/react-dom": "18.0.8",
"eslint": "8.27.0",
"eslint-config-next": "13.0.2",
"next": "13.0.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "4.8.4"
}
}

56
pages/_app.tsx Normal file
View File

@ -0,0 +1,56 @@
import * as React from "react";
import type { AppProps } from "next/app";
import { CacheProvider, EmotionCache } from "@emotion/react";
import { ThemeProvider, CssBaseline, createTheme } from "@mui/material";
import useMediaQuery from "@mui/material/useMediaQuery";
import "@fontsource/roboto/300.css";
import "@fontsource/roboto/400.css";
import "@fontsource/roboto/500.css";
import "@fontsource/roboto/700.css";
import createEmotionCache from "../createEmotionCache";
import { lightThemeOptions, darkThemeOptions } from "../styles/theme";
import "../styles/globals.css";
import { Layout } from "../components/layout";
import Head from "next/head";
interface MyAppProps extends AppProps {
emotionCache?: EmotionCache;
}
const clientSideEmotionCache = createEmotionCache();
const lightTheme = createTheme(lightThemeOptions);
const darkTheme = createTheme(darkThemeOptions);
const MyApp: React.FunctionComponent<MyAppProps> = (props) => {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
const userPrefersDarkmode = useMediaQuery("(prefers-color-scheme: dark)");
const getContent = () => {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
};
return (
<>
<Head>
<title>noogle</title>
<meta name="description" content="Search nix functions" />
<link rel="icon" href="/favicon.png" />
</Head>
<CacheProvider value={emotionCache}>
<ThemeProvider theme={userPrefersDarkmode ? darkTheme : lightTheme}>
<CssBaseline />
{getContent()}
</ThemeProvider>
</CacheProvider>
</>
);
};
export default MyApp;

450
pages/index.tsx Normal file
View File

@ -0,0 +1,450 @@
import { BasicList, BasicListItem } from "../components/basicList";
import { useState, useMemo } from "react";
import { Box } from "@mui/material";
import FunctionItem from "../components/functionItem/functionItem";
import { NixType } from "../types/basicDataView";
import { Preview } from "../components/preview/preview";
const libfuns = {
mapAttrs: {
"attr-path": "builtins.mapAttrs",
"doc-url":
"https://nixos.org/manual/nix/stable/language/builtins.html#builtins-mapAttrs",
source: "builtin",
from: "attrset",
to: "attrset",
},
"mapAttrs'": {
"attr-path": "lib.mapAttrs'",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-attrset",
source: "nixpkgs",
from: "attrset",
to: "attrset",
},
toString: {
"attr-path": "builtins.toString'",
"doc-url":
"https://nixos.org/manual/nix/stable/language/builtins.html#builtins-toString",
source: "builtin",
from: "any",
to: "string",
},
genAttrs: {
"attr-path": "lib.genAttrs",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-attrset",
source: "nixpkgs",
from: "list",
to: "attrset",
},
forEach: {
"attr-path": "lib.forEach",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
foldr: {
"attr-path": "lib.foldr",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "any",
},
foldl: {
"attr-path": "lib.foldl",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "any",
},
"foldl'": {
"attr-path": "lib.foldl'",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "any",
},
imap0: {
"attr-path": "lib.imap0",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
imap1: {
"attr-path": "lib.imap1",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
concatMap: {
"attr-path": "lib.concatMap",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
flatten: {
"attr-path": "lib.flatten",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
remove: {
"attr-path": "lib.remove",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
findSingle: {
"attr-path": "lib.findSingle",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "any",
},
findFirst: {
"attr-path": "lib.findFirst",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "any",
},
any: {
"attr-path": "lib.any",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "true",
},
all: {
"attr-path": "lib.all",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "true",
},
count: {
"attr-path": "lib.count",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "int",
},
optional: {
"attr-path": "lib.optional",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "bool",
to: "list",
},
optionals: {
"attr-path": "lib.optionals",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "bool",
to: "list",
},
toList: {
"attr-path": "lib.toList",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "any",
to: "list",
},
range: {
"attr-path": "lib.range",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "int",
to: "list",
},
partition: {
"attr-path": "lib.partition",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
"groupBy'": {
"attr-path": "lib.groupBy'",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
groupBy: {
"attr-path": "lib.groupBy",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
zipListsWith: {
"attr-path": "lib.zipListsWith",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
zipLists: {
"attr-path": "lib.zipLists",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
reverseList: {
"attr-path": "lib.reverseList",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
listDfs: {
"attr-path": "lib.listDfs",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "attrset",
},
toposort: {
"attr-path": "lib.toposort",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
sort: {
"attr-path": "lib.sort",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
compareLists: {
"attr-path": "lib.compareLists",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "int",
},
naturalSort: {
"attr-path": "lib.naturalSort",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
take: {
"attr-path": "lib.take",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
sublist: {
"attr-path": "lib.sublist",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
last: {
"attr-path": "lib.last",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "any",
},
init: {
"attr-path": "lib.init",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
crossLists: {
"attr-path": "lib.crossLists",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
unique: {
"attr-path": "lib.unique",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
intersectLists: {
"attr-path": "lib.intersectLists",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
subtractLists: {
"attr-path": "lib.subtractLists",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "list",
},
mutuallyExclusive: {
"attr-path": "lib.mutuallyExclusive",
"doc-url":
"https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-lists",
source: "nixpkgs",
from: "list",
to: "bool",
},
};
export type FuncData = {
name: string;
info: {
"attr-path": string;
"doc-url": string;
source: string;
from: string;
to: string;
};
};
const mockData: FuncData[] = Object.entries(libfuns).map(([key, value]) => ({
name: key,
info: value,
}));
const pipe =
(...fns: ((arr: FuncData[]) => FuncData[])[]) =>
(x: FuncData[]) =>
fns.reduce((v, f) => f(v), x);
const search =
(term: string) =>
(data: FuncData[]): FuncData[] => {
return data.filter((item) => {
return Object.values(item).some((value) => {
const valueAsString = value.toString();
return valueAsString.toLowerCase().includes(term.toLocaleLowerCase());
});
});
};
const filter =
(to: NixType, from: NixType) =>
(data: FuncData[]): FuncData[] => {
return data.filter(({ info }) => info.from === from && info.to === to);
};
export default function FunctionsPage() {
// const [visibleFuncs, setVisibleFuncs] = useState(mockData);
const [selected, setSelected] = useState<string | null>(null);
const [term, setTerm] = useState<string>("");
const [to, setTo] = useState<NixType>("any");
const [from, setFrom] = useState<NixType>("any");
const handleSelect = (key: string) => {
setSelected((curr) => {
if (curr === key) {
return null;
} else {
return key;
}
});
};
const filteredData = useMemo(
() => pipe(filter(to, from), search(term))(mockData),
[to, from, term]
);
const handleSearch = (term: string) => {
setTerm(term);
};
const handleFilter = (t: NixType, mode: "to" | "from") => {
if (mode === "from") {
setFrom(t);
}
if (mode === "to") {
setTo(t);
}
};
const preRenderedItems: BasicListItem[] = filteredData.map(
({ name, info }) => ({
item: (
<Box
sx={{
width: "100%",
height: "100%",
}}
onClick={() => handleSelect(name)}
>
<FunctionItem name={name} info={info} selected={selected === name} />
</Box>
),
key: name,
})
);
const preview = (
<Preview func={mockData.find((f) => f.name === selected) || mockData[0]} />
);
return (
<Box sx={{ ml: 2 }}>
<BasicList
items={preRenderedItems}
handleSearch={handleSearch}
handleFilter={handleFilter}
preview={selected ? preview : null}
/>
</Box>
);
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

4
public/vercel.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

21
styles/globals.css Normal file
View File

@ -0,0 +1,21 @@
html,
body {
padding: 0;
margin: 0;
background-color: rgb(242, 248, 253);
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
.react-terminal-wrapper:before {
background: unset !important;
}

View File

@ -0,0 +1,16 @@
import { ThemeOptions } from "@mui/material/styles";
const darkThemeOptions: ThemeOptions = {
palette: {
mode: "dark",
primary: {
main: "#6586c8"
},
secondary: {
main: "#6ad541"
}
},
};
export { darkThemeOptions };

2
styles/theme/index.tsx Normal file
View File

@ -0,0 +1,2 @@
export { lightThemeOptions } from "./lightThemeOptions";
export { darkThemeOptions } from "./darkThemeOptions";

View File

@ -0,0 +1,16 @@
import { ThemeOptions } from "@mui/material/styles";
const lightThemeOptions: ThemeOptions = {
palette: {
mode: "light",
primary: {
main: "#6586c8"
},
secondary: {
main: "#6ad541"
}
},
};
export { lightThemeOptions };

20
tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

12
types/basicDataView.ts Normal file
View File

@ -0,0 +1,12 @@
export type BasicDataItem = {
item: React.ReactNode;
key: string;
};
export type BasicDataViewProps = {
items: BasicDataItem[];
pageCount?: number;
handleSearch?: (term: string) => void;
};
export type NixType = "attrset" | "list" | "string" | "int" | "bool" | "any";