Skip to content

Commit

Permalink
feat: init ui
Browse files Browse the repository at this point in the history
  • Loading branch information
vharny committed Jan 24, 2024
1 parent 1988884 commit 40e8e14
Show file tree
Hide file tree
Showing 21 changed files with 2,600 additions and 0 deletions.
18 changes: 18 additions & 0 deletions ui/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
24 changes: 24 additions & 0 deletions ui/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
30 changes: 30 additions & 0 deletions ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:

- Configure the top-level `parserOptions` property like this:

```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
```

- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
13 changes: 13 additions & 0 deletions ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
33 changes: 33 additions & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "search-engine",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@heroicons/react": "^2.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.3"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.17",
"eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1",
"typescript": "^5.2.2",
"vite": "^5.0.8"
}
}
6 changes: 6 additions & 0 deletions ui/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
1 change: 1 addition & 0 deletions ui/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import Results from "./pages/Results";

const App = () => (
<Routes>
<Route index path="/" element={<Home />} />
<Route path="/search" element={<Results />} />
</Routes>
);

export default App;
1 change: 1 addition & 0 deletions ui/src/assets/github.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions ui/src/components/Loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
type Props = {
className?: string;
};

const Loader = ({ className }: Props) => (
<div className={className}>
<svg
className="animate-spin -ml-1 mr-3 h-8 w-8 text-indigo-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
</div>
);

export default Loader;
3 changes: 3 additions & 0 deletions ui/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
13 changes: 13 additions & 0 deletions ui/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App.tsx";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
42 changes: 42 additions & 0 deletions ui/src/pages/Home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
import viteLogo from "/vite.svg";
import { useNavigate } from "react-router-dom";

const Home = () => {
const navigate = useNavigate();

const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
const query = form.get("query");
navigate(`/search?q=${query}&p=1`);
};

return (
<form className="mt-36 md:mt-64 flex flex-col" onSubmit={handleSubmit}>
<img className="mx-auto mb-4" src={viteLogo} alt="Logo" width="120" />
<h1 className="mb-8 text-xl text-center">Search Engine</h1>
<div className="mx-auto w-4/5 md:w-1/2 max-w-lg mb-4 relative">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<MagnifyingGlassIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</div>
<input
type="text"
className="block w-full rounded-md border-0 py-1.5 pl-10 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="query"
/>
</div>
<button
type="submit"
className="mx-auto w-48 rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
>
Search
</button>
</form>
);
};

export default Home;
119 changes: 119 additions & 0 deletions ui/src/pages/Results.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Link, useNavigate, useSearchParams } from "react-router-dom";
import { useEffect, useState } from "react";
import Loader from "../components/Loader";
import githubLogo from "../assets/github.svg";
import { Result } from "../types";

const SIZE = 10;

const Results = () => {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const [result, setResult] = useState<Result>();
const query = searchParams.get("q");
const page = searchParams.get("p");
const from = (Number(page) - 1) * SIZE;

useEffect(() => {
if (query && page) {
fetch(`http://localhost:8080/search?q=${query}&from=${from}&size=${SIZE}`)
.then((response) => response.json())
.then((result) => setResult(result));
} else {
navigate("/");
}
}, [navigate, from, query, page]);

const handlePagination = (type: "previous" | "next"): void => {
const newPage = type === "previous" ? Number(page) - 1 : Number(page) + 1;
setSearchParams({ q: query!, p: String(newPage) });
};

return (
<div className="container px-4 mt-8">
<Link className="flex items-center space-x-2 mb-4 w-fit" to="/">
<img src="/vite.svg" alt="Logo" />
<h1 className="text-xl">Search Engine</h1>
</Link>
{result ? (
<div className="overflow-hidden rounded-md border border-gray-300 bg-white my-6">
<ul role="list" className="divide-y divide-gray-300">
{result.hits.map((hit) => (
<li key={hit.id} className="px-6 py-4 space-y-2">
{/* Title */}
<a
className="flex items-center space-x-2 w-fit"
href={hit.fields.url}
target="_blank"
>
<img
src={githubLogo}
width="20"
alt={hit.fields.name_with_owner}
/>
<h2 className="font-bold truncate">
{hit.fields.name_with_owner}
</h2>
<span
className="inline-flex items-center gap-x-1.5 rounded-md px-1.5 py-0.5 text-xs font-medium text-white"
style={{
backgroundColor: hit.fields["primary_language.color"],
}}
>
<svg
className="h-1.5 w-1.5 fill-white"
viewBox="0 0 6 6"
aria-hidden="true"
>
<circle cx={3} cy={3} r={3} />
</svg>
{hit.fields["primary_language.name"]}
</span>
</a>
<p>{hit.fields.description}</p>
</li>
))}
</ul>
<nav
className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6"
aria-label="Pagination"
>
<div className="hidden sm:block">
<p className="text-sm text-gray-700">
Showing <span className="font-medium">{from}</span> to{" "}
<span className="font-medium">{from + SIZE}</span> of{" "}
<span className="font-medium">{result.total_hits}</span> results
(
<span className="font-medium">
{(result.took / 1000).toFixed(1)}
</span>{" "}
seconds)
</p>
</div>
<div className="flex flex-1 justify-between sm:justify-end space-x-3">
<button
type="button"
className="relative inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0 disabled:bg-gray-100 disabled:cursor-not-allowed"
onClick={() => handlePagination("previous")}
disabled={page === "1"}
>
Previous
</button>
<button
type="button"
className="relative inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0 disabled:cursor-not-allowed"
onClick={() => handlePagination("next")}
>
Next
</button>
</div>
</nav>
</div>
) : (
<Loader className="flex justify-center my-8" />
)}
</div>
);
};

export default Results;
Loading

0 comments on commit 40e8e14

Please sign in to comment.