initial commit
This commit is contained in:
commit
72fcb3e49f
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
34
README.md
Normal file
34
README.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
## Usage
|
||||||
|
|
||||||
|
Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
|
||||||
|
|
||||||
|
This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install # or pnpm install or yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm run dev` or `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.<br>
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||||
|
|
||||||
|
The page will reload if you make edits.<br>
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `dist` folder.<br>
|
||||||
|
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.<br>
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
|
25
index.html
Normal file
25
index.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<link rel="shortcut icon" type="image/svg" href="/src/assets/favicon.svg" />
|
||||||
|
<title>Search</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
|
||||||
|
<script src="/src/index.tsx" type="module"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
3053
index.json
Normal file
3053
index.json
Normal file
File diff suppressed because one or more lines are too long
1957
package-lock.json
generated
Normal file
1957
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-template-solid",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "",
|
||||||
|
"scripts": {
|
||||||
|
"start": "vite",
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"serve": "vite preview"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"solid-devtools": "^0.27.3",
|
||||||
|
"typescript": "^5.1.3",
|
||||||
|
"vite": "^4.3.9",
|
||||||
|
"vite-plugin-solid": "^2.7.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"solid-js": "^1.7.6"
|
||||||
|
}
|
||||||
|
}
|
1190
pnpm-lock.yaml
Normal file
1190
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
45
src/App.module.css
Normal file
45
src/App.module.css
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
.app {
|
||||||
|
position: relative;
|
||||||
|
--bg: #111111;
|
||||||
|
--element-bg: #333333;
|
||||||
|
|
||||||
|
background-color: var(--bg);
|
||||||
|
min-height: 100vh;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
height: 20px;
|
||||||
|
background: red;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressOuter {
|
||||||
|
position: relative;
|
||||||
|
height: 20px;
|
||||||
|
background: gray;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result>a {
|
||||||
|
color: #88ccff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result>a:visited {
|
||||||
|
color: gray;
|
||||||
|
}
|
116
src/App.tsx
Normal file
116
src/App.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { createEffect, type Component, Show, createSignal, onMount, on, For } from 'solid-js';
|
||||||
|
import { Icon, SvgIcon } from "@suid/material";
|
||||||
|
|
||||||
|
import styles from './App.module.css';
|
||||||
|
|
||||||
|
import Search from './components/Search';
|
||||||
|
import { render } from 'solid-js/web';
|
||||||
|
|
||||||
|
interface SearchResult {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Index {
|
||||||
|
[key: string]: IndexEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IndexEntry {
|
||||||
|
url: string;
|
||||||
|
normalizedValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalize(text: string): string {
|
||||||
|
return text.toLowerCase().replace(/[\t\r\n ]/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const App: Component = () => {
|
||||||
|
const [loading, setLoading] = createSignal(true);
|
||||||
|
const [progress, setProgress] = createSignal(0);
|
||||||
|
const [index, setIndex] = createSignal({} as Index);
|
||||||
|
const [results, setResults] = createSignal([] as SearchResult[]);
|
||||||
|
const [query, setQuery] = createSignal("");
|
||||||
|
|
||||||
|
createEffect(on([index, query], () => {
|
||||||
|
let results = [] as SearchResult[];
|
||||||
|
|
||||||
|
let idx = index();
|
||||||
|
let normalizedQuery = normalize(query());
|
||||||
|
|
||||||
|
for (let key in idx) {
|
||||||
|
let value = idx[key];
|
||||||
|
if (key.includes(normalizedQuery) || value.normalizedValue.includes(normalizedQuery)) {
|
||||||
|
results.push({
|
||||||
|
name: key,
|
||||||
|
url: value.url
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setResults(results);
|
||||||
|
}));
|
||||||
|
|
||||||
|
createEffect(async () => {
|
||||||
|
const indexResult = await fetch("index.json");
|
||||||
|
const totalLength = parseInt(indexResult.headers.get("Content-Length") || "-1");
|
||||||
|
const reader = indexResult.body?.getReader();
|
||||||
|
|
||||||
|
if (!reader || totalLength == -1) {
|
||||||
|
// Error msg
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: Uint8Array = new Uint8Array(totalLength);
|
||||||
|
let position = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
setLoading(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.set(value, position);
|
||||||
|
position += value.length;
|
||||||
|
setProgress(progress() + value.length / totalLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize index
|
||||||
|
let rawIndex = JSON.parse(new TextDecoder("utf-8").decode(data));
|
||||||
|
let normalizedIndex = {} as Index;
|
||||||
|
for (let key in rawIndex) {
|
||||||
|
normalizedIndex[normalize(key)] = {
|
||||||
|
url: key,
|
||||||
|
normalizedValue: normalize(rawIndex[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIndex(normalizedIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={styles.app}>
|
||||||
|
<div class={styles.appContainer}>
|
||||||
|
<h2>Search for something</h2>
|
||||||
|
<Search placeholder='Search' onChange={setQuery} doneTypingTimeout={200} />
|
||||||
|
|
||||||
|
<Show when={loading()}>
|
||||||
|
<div class={styles.loading}>
|
||||||
|
<span>Loading search index...</span>
|
||||||
|
<div class={styles.progressOuter}>
|
||||||
|
<div class={styles.progress} style={{ width: (progress() * 100) + "%" }}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={!loading()}>
|
||||||
|
<ul class={styles.results}>
|
||||||
|
<For each={results()}>{item => <li class={styles.result}><a href={item.url} target='_blank'>{item.name}</a></li>}</For>
|
||||||
|
</ul>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div >
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
1
src/assets/favicon.svg
Normal file
1
src/assets/favicon.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#FFFFFF"><path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z" /></svg>
|
After Width: | Height: | Size: 352 B |
49
src/components/Search.module.css
Normal file
49
src/components/Search.module.css
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
.searchLabel {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
--radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchBox {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background-color: var(--element-bg);
|
||||||
|
color: white;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchBox[data-suggestions]:focus {
|
||||||
|
border-radius: var(--radius) var(--radius) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchSuggestions {
|
||||||
|
background-color: var(--element-bg);
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0 0 var(--radius) var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchSuggestions>li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 25px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchSuggestions>li:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchSuggestions:empty::after {
|
||||||
|
content: "No suggestions";
|
||||||
|
color: gray;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
62
src/components/Search.tsx
Normal file
62
src/components/Search.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { createSignal, type Component, JSX, For, createEffect, on } from "solid-js";
|
||||||
|
|
||||||
|
import styles from './Search.module.css'
|
||||||
|
import { render } from "solid-js/web";
|
||||||
|
|
||||||
|
interface SearchProps {
|
||||||
|
placeholder: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
//style?: string | JSX.CSSProperties;
|
||||||
|
suggestions?: string[];
|
||||||
|
doneTypingTimeout?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Search: Component<SearchProps> = (props: SearchProps) => {
|
||||||
|
let [suggestions, setSuggestions] = createSignal(props.suggestions);
|
||||||
|
let [suggestionsVisible, setSuggestionsVisible] = createSignal(false);
|
||||||
|
let [query, setQuery] = createSignal("");
|
||||||
|
|
||||||
|
const doneTypingTimeout = props.doneTypingTimeout || 0;
|
||||||
|
let doneTimeout = 0;
|
||||||
|
|
||||||
|
const onFocus: JSX.EventHandler<HTMLInputElement, FocusEvent> = e => {
|
||||||
|
if (props.suggestions) setSuggestionsVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBlur: JSX.EventHandler<HTMLInputElement, FocusEvent> = e => {
|
||||||
|
if (props.suggestions) setSuggestionsVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitValue = (query: string) => {
|
||||||
|
console.log("change", query);
|
||||||
|
props.onChange?.(query);
|
||||||
|
|
||||||
|
if (props.suggestions) {
|
||||||
|
let newSuggestions = props.suggestions.filter(v => v.toLowerCase().includes(query.toLowerCase()));
|
||||||
|
setSuggestions(newSuggestions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createEffect(on(query, query => {
|
||||||
|
if (doneTypingTimeout > 0) {
|
||||||
|
clearTimeout(doneTimeout);
|
||||||
|
doneTimeout = setTimeout(() => submitValue(query), doneTypingTimeout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitValue(query);
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={styles.search}>
|
||||||
|
<input class={styles.searchBox} placeholder={props.placeholder} data-suggestions={props.suggestions ? true : undefined} onInput={e => setQuery(e.currentTarget.value)} onFocus={onFocus} onBlur={onBlur} value={query()}></input>
|
||||||
|
<ul class={styles.searchSuggestions} style={{ display: (suggestionsVisible() ? "unset" : "none") }}>
|
||||||
|
<For each={suggestions()}>{item => <li onMouseDown={() => {
|
||||||
|
setQuery(item);
|
||||||
|
}}>{item}</li>}</For>
|
||||||
|
</ul>
|
||||||
|
</div >
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Search;
|
13
src/index.css
Normal file
13
src/index.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
15
src/index.tsx
Normal file
15
src/index.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/* @refresh reload */
|
||||||
|
import { render } from 'solid-js/web';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
const root = document.getElementById('root');
|
||||||
|
|
||||||
|
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||||
|
throw new Error(
|
||||||
|
'Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(() => <App />, root!);
|
1
src/logo.svg
Normal file
1
src/logo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 166 155.3"><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" fill="#76b3e1"/><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="27.5" y1="3" x2="152" y2="63.5"><stop offset=".1" stop-color="#76b3e1"/><stop offset=".3" stop-color="#dcf2fd"/><stop offset="1" stop-color="#76b3e1"/></linearGradient><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" opacity=".3" fill="url(#a)"/><path d="M52 35l-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" fill="#518ac8"/><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="95.8" y1="32.6" x2="74" y2="105.2"><stop offset="0" stop-color="#76b3e1"/><stop offset=".5" stop-color="#4377bb"/><stop offset="1" stop-color="#1f3b77"/></linearGradient><path d="M52 35l-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" opacity=".3" fill="url(#b)"/><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="18.4" y1="64.2" x2="144.3" y2="149.8"><stop offset="0" stop-color="#315aa9"/><stop offset=".5" stop-color="#518ac8"/><stop offset="1" stop-color="#315aa9"/></linearGradient><path d="M134 80a45 45 0 00-48-15L24 85 4 120l112 19 20-36c4-7 3-15-2-23z" fill="url(#c)"/><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="75.2" y1="74.5" x2="24.4" y2="260.8"><stop offset="0" stop-color="#4377bb"/><stop offset=".5" stop-color="#1a336b"/><stop offset="1" stop-color="#1a336b"/></linearGradient><path d="M114 115a45 45 0 00-48-15L4 120s53 40 94 30l3-1c17-5 23-21 13-34z" fill="url(#d)"/></svg>
|
After Width: | Height: | Size: 1.6 KiB |
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"noEmit": true,
|
||||||
|
"isolatedModules": true
|
||||||
|
}
|
||||||
|
}
|
21
vite.config.ts
Normal file
21
vite.config.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import solidPlugin from 'vite-plugin-solid';
|
||||||
|
// import devtools from 'solid-devtools/vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
/*
|
||||||
|
Uncomment the following line to enable solid-devtools.
|
||||||
|
For more info see https://github.com/thetarnav/solid-devtools/tree/main/packages/extension#readme
|
||||||
|
*/
|
||||||
|
// devtools(),
|
||||||
|
solidPlugin(),
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
base: './'
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user