Compare commits

...

47 Commits
main ... 5.1.0

Author SHA1 Message Date
65d4e76fa7 feature: i18n support 2024-06-25 02:10:37 +08:00
844333e623 ref: sparkhome v5, use vite instead of next.js
Progress: component search, time and background could run.
2024-06-24 04:40:31 +08:00
Alikia2x
13e79e2869 clean: remove unnecessary files, full i18n for base64 tools 2024-06-19 17:32:50 +08:00
Alikia2x
421e4fcdb8 feature: intention detection by nlp.js, with demo of showing weather 2024-06-19 17:14:23 +08:00
Alikia2x
669ad510ed merge: branch dev into ref-rule-nlp 2024-06-17 18:52:03 +08:00
Alikia2x
5519d313ee add: document by vitepress 2024-06-17 18:51:49 +08:00
Alikia2x
e7f6f69dfe temp: NLP base class 2024-06-17 03:11:17 +08:00
Alikia2x
f85162622d fix: style of suggestion item 2024-06-14 19:55:29 +08:00
Alikia2x
24f23b3573 improve: base64 tool 2024-05-15 00:42:04 +08:00
Alikia2x
ff0d05542d feature: base64 tool page 2024-05-05 16:28:12 +08:00
Alikia2x
969ed948b5 improve: onesearch relevance display 2024-04-29 15:26:21 +08:00
Alikia2x
e3ec2bb897 page: tools page: base64 2024-04-27 19:00:47 +08:00
Alikia2x
6fe4fc28c1 feature: add log and error report 2024-04-27 17:00:02 +08:00
Alikia2x
1faee002a9 improve: better base64 check, with test 2024-04-27 16:58:37 +08:00
Alikia2x
38b144a6f4 improve: prompt for NLP result 2024-04-26 22:43:47 +08:00
Alikia2x
86d4015ae5 feature: better base64NLP, add copy to clipboard for text 2024-04-26 22:20:49 +08:00
Alikia2x
b46f1adddb improve: base64 NLP 2024-04-26 18:21:52 +08:00
Alikia2x
1d7aaf6a8a feature: base64 detect and auto-decode 2024-04-26 18:11:29 +08:00
Alikia2x
136450f93d feature: base structure of onesearch, no bugs now 2024-04-26 18:10:31 +08:00
Alikia2x
a9cf5630fe feature: complete function and code logic for suggestion (include cloud)
todo: problem: In a short period of time, several functions that depend on the latest state at the same time do operations and update the state according to the state they got, which will cause all calls except the first one to update the state incorrectly due to incorrect dependencies (not-updated state) because of the asynchronous updating of the React state.
2024-04-17 23:44:28 +08:00
Alikia2x
a136c0ab9b preparing: ready to refactor onesearch effect logic 2024-04-13 20:30:20 +08:00
Alikia2x
50c526c095 type: add suggestionItem 2024-04-13 20:23:06 +08:00
Alikia2x
7dd7b70db8 improve: suggestion, modify type decleartion
Suggestion now shows a default search option same as query, this behaviour is same as major browsers.
2024-04-12 21:29:12 +08:00
Alikia2x
f1d22f9d91 improvement: suggestion feature done, adjust i18n language key 2024-04-10 17:34:44 +08:00
Alikia2x
04d72d3ca8 feature: search auto complete API 2024-04-10 00:11:08 +08:00
Alikia2x
8e65767ac1 preparing: browser history search feature 2024-04-06 14:28:41 +08:00
Alikia2x
ced6bffc28 fix: unexpected load settings 2024-04-05 02:20:44 +08:00
Alikia2x
67b5f47440 fixed: hydration error in Time and Background 2024-04-05 01:02:39 +08:00
Alikia2x
7d412ad439 debug: hydration error 2024-04-05 00:09:55 +08:00
Alikia2x
5ca8d32778 fix: nextui tailwind error on docker 2024-04-04 23:02:25 +08:00
Alikia2x
1ab4f1ce63 feature: docker support 2024-04-04 22:51:59 +08:00
Alikia2x
6bcf445848 structure: base components of search suggestions 2024-04-04 21:49:58 +08:00
Alikia2x
8223e696f9 components: Search suggestions 2024-04-04 17:49:03 +08:00
Alikia2x
8a9ef8fcb4 structure: backend API 2024-04-04 13:43:00 +08:00
Alikia2x
b3255de375 interface: right position for engineSelector 2024-04-04 02:39:41 +08:00
Alikia2x
09a7941eca feature: engine change & nextui 2024-04-04 02:26:37 +08:00
Alikia2x
090154115a improve: Time component with props 2024-04-02 01:14:39 +08:00
Alikia2x
0aef1e1641 improvement: text for searchbox placeholder 2024-03-31 17:21:26 +08:00
Alikia2x
c1930d6bd7 imporve: test for validLink 2024-03-31 17:06:05 +08:00
Alikia2x
24de7bcaa3 fix: remove redundant log in validLink and rename 2024-03-31 16:29:51 +08:00
Alikia2x
62a0e85554 test: add test for validLink 2024-03-31 16:26:48 +08:00
Alikia2x
1f5a36832c imporvement: better link check 2024-03-31 16:26:33 +08:00
Alikia2x
09d099a625 structure: add favicon 2024-03-31 14:14:58 +08:00
Alikia2x
a5f5112fc1 feature: directly open URL 2024-03-31 14:02:42 +08:00
Alikia2x
8c005b3769 interface: better theme & state 2024-03-31 01:35:55 +08:00
Alikia2x
9a623164e4 feature: time 2024-03-31 01:07:38 +08:00
Alikia2x
304ea14003 feature: i18n support 2024-03-31 00:56:04 +08:00
55 changed files with 3593 additions and 1045 deletions

18
.eslintrc.cjs Normal file
View File

@ -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 },
],
},
}

34
.gitignore vendored
View File

@ -1,3 +1,28 @@
# 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?
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies # dependencies
@ -34,4 +59,11 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
.syncignore .syncignore
# log
app.log
# doc
doc/.vitepress/dist
doc/.vitepress/cache

View File

@ -5,5 +5,4 @@
"singleQuote": false, "singleQuote": false,
"printWidth": 120, "printWidth": 120,
"endOfLine": "lf" "endOfLine": "lf"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,33 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}

View File

@ -1,22 +0,0 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./global.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app"
};
export default function RootLayout({
children
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}

View File

@ -1,12 +0,0 @@
"use client";
import { RecoilRoot } from "recoil";
import Homepage from "../components";
export default function Home() {
return (
<RecoilRoot>
<Homepage />
</RecoilRoot>
);
}

View File

@ -1,41 +1,34 @@
import Image from "next/image"; import { useEffect, useState } from "react";
import { useRecoilValue } from "recoil"; import { useAtom } from "jotai";
import { settingsState } from "./state/settings"; import { bgFocusAtom } from "../lib/state/background";
import validUrl from "valid-url"; import BackgroundContainer from "./backgroundContainer";
import validateColor from "validate-color";
function Background(props: { export default function Background() {
isFocus: boolean; const [isFocus, setFocus] = useAtom(bgFocusAtom);
src: string; const [darkMode, setDarkMode] = useState(false);
onClick: () => void;
}) { useEffect(() => {
const settings = useRecoilValue(settingsState); const colorSchemeQueryList = window.matchMedia("(prefers-color-scheme: dark)");
if (validateColor(props.src)) { setDarkMode(colorSchemeQueryList.matches ? true : false);
return (
<div const handleChange = () => {
className="w-full h-full fixed object-cover inset-0 duration-200 z-0" setDarkMode(colorSchemeQueryList.matches ? true : false);
style={{ backgroundColor: props.src }} };
onClick={props.onClick}
></div> colorSchemeQueryList.addEventListener("change", handleChange);
);
} else if (validUrl.isWebUri(props.src)) { return () => {
return ( colorSchemeQueryList.removeEventListener("change", handleChange);
<Image };
src={props.src} }, []);
className={
"w-full h-full fixed object-cover inset-0 duration-200 z-0 " + return (
(props.isFocus <div>
? settings.bgBlur {darkMode ? (
? "blur-lg scale-110" <BackgroundContainer src="rgb(23,25,29)" isFocus={isFocus} onClick={() => setFocus(false)} darkMode={darkMode}/>
: "brightness-50 scale-105" ) : (
: "") <BackgroundContainer src="white" isFocus={isFocus} onClick={() => setFocus(false)} darkMode={darkMode}/>
} )}
alt="background" </div>
onClick={props.onClick} );
fill={true}
/>
);
}
} }
export default Background;

View File

@ -0,0 +1,50 @@
import { settingsAtom } from "lib/state/settings";
import validUrl from "valid-url";
import validateColor from "validate-color";
import { useAtomValue } from "jotai";
export default function BackgroundContainer(props: {
isFocus: boolean;
src: string;
darkMode: boolean;
onClick: () => void;
}) {
const settings = useAtomValue(settingsAtom);
if (validateColor(props.src)) {
return (
<div
className="w-full h-full fixed object-cover inset-0 duration-200 z-0"
style={{ backgroundColor: props.src }}
onClick={props.onClick}
></div>
);
} else if (validUrl.isWebUri(props.src)) {
return (
<img
src={props.src}
className={
"w-full h-full fixed object-cover inset-0 duration-200 z-0 " +
(props.isFocus ? (settings.bgBlur ? "blur-lg scale-110" : "brightness-50 scale-105") : "")
}
alt="background"
onClick={props.onClick}
/>
);
} else {
if (props.darkMode) {
return (
<div
className="w-full h-full fixed object-cover inset-0 duration-200 z-0 bg-[rgb(23,25,29)]"
onClick={props.onClick}
></div>
);
} else {
return (
<div
className="w-full h-full fixed object-cover inset-0 duration-200 z-0 bg-white"
onClick={props.onClick}
></div>
);
}
}
}

View File

@ -1,23 +0,0 @@
import { useRecoilState } from "recoil";
import Background from "./background";
import Search from "./search/search";
import { bgFocusState } from "./state/background";
export default function Homepage() {
const [isFocus, setFocus] = useRecoilState(bgFocusState);
return (
<div className="h-full fixed overflow-hidden w-full bg-black">
<Background
src="rgb(23,25,29)"
isFocus={isFocus}
onClick={() => setFocus(false)}
/>
<Search
onFocus={() => {
setFocus(true);
console.log("focus");
}}
/>
</div>
);
}

89
components/search.tsx Normal file
View File

@ -0,0 +1,89 @@
import { KeyboardEvent, useRef } from "react";
import { useAtom, useAtomValue } from "jotai";
import { settingsAtom } from "lib/state/settings";
import { queryAtom } from "lib/state/query";
import { selectedSuggestionAtom } from "lib/state/suggestionSelection";
import handleEnter from "lib/onesearch/handleEnter";
import { suggestionAtom } from "lib/state/suggestion";
import { useTranslation } from "react-i18next";
export default function Search(props: { onFocus: () => void }) {
const { t } = useTranslation();
const settings = useAtomValue(settingsAtom);
const [query, setQuery] = useAtom(queryAtom);
const [selectedSuggestion, setSelected] = useAtom(selectedSuggestionAtom);
const suggestions = useAtomValue(suggestionAtom);
const searchBoxRef = useRef<HTMLInputElement>(null);
const style = "default";
function handleKeydown(e: KeyboardEvent) {
if (e.key == "Enter") {
e.preventDefault();
handleEnter(selectedSuggestion, suggestions, query, settings, searchBoxRef);
return;
} else if (e.key == "ArrowUp") {
e.preventDefault();
const len = suggestions.length;
setSelected((selectedSuggestion - 1 + len) % len);
} else if (e.key == "ArrowDown") {
e.preventDefault();
const len = suggestions.length;
setSelected((selectedSuggestion + 1) % len);
}
}
if (style === "default") {
return (
// 祖传样式,勿动
<div className="absolute w-full top-[8.5rem] lg:top-56 short:top-24 z-1 left-1/2 translate-x-[-50%] ">
<input
className="absolute z-1 w-11/12 sm:w-[700px] h-10 rounded-lg left-1/2 translate-x-[-50%]
text-center outline-none border-[1px] focus:border-2 duration-200 pr-2 shadow-md focus:shadow-none dark:shadow-zinc-800 dark:shadow-md bg-white dark:bg-[rgb(23,25,29)]
dark:border-neutral-500 dark:focus:border-neutral-300 placeholder:text-slate-500
dark:placeholder:text-slate-400 text-slate-900 dark:text-white"
id="searchBox"
type="text"
placeholder={t('search.placeholder')}
onFocus={props.onFocus}
onKeyDown={handleKeydown}
onChange={(e) =>
setQuery(() => {
return e.target.value;
})
}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
ref={searchBoxRef}
value={query}
/>
</div>
);
} else if (style == "image") {
return (
// 祖传样式,勿动
<div className="absolute w-full top-[8.5rem] lg:top-56 short:top-24 z-1 left-1/2 translate-x-[-50%] ">
<input
className={
`absolute z-1 w-2/3 sm:w-80 md:w-[400px] focus:w-11/12 focus:sm:w-[700px] hover:w-11/12
hover:sm:w-[700px] h-10 rounded-3xl left-1/2 translate-x-[-50%] text-center outline-none
border-solid border-0 duration-200 pr-2 shadow-md focus:shadow-none` +
(settings.bgBlur
? `bg-[rgba(255,255,255,0.5)] dark:bg-[rgba(24,24,24,0.75)] backdrop-blur-xl
placeholder:text-slate-500 dark:placeholder:text-slate-400 text-slate-900 dark:text-white`
: `bg-[rgba(235,235,235,0.9)] dark:bg-[rgba(20,20,20,0.9)] placeholder:text-slate-500
text-slate-800 dark:text-white`)
}
id="searchBox"
type="text"
placeholder="placeholder"
onFocus={props.onFocus}
ref={searchBoxRef}
/>
</div>
);
}
}

View File

@ -1,48 +0,0 @@
import { atom, useRecoilValue } from "recoil";
import { settingsState } from "../state/settings";
export default function Search(props: {
onFocus: () => void;
}) {
const settings = useRecoilValue(settingsState);
let style = "default";
if (style === "default") {
return (
// 祖传样式,勿动
<div className="absolute w-full top-[8.5rem] lg:top-56 short:top-24 z-1 left-1/2 translate-x-[-50%] ">
<input
className="absolute z-1 w-11/12 sm:w-[700px] h-10 rounded-lg left-1/2 translate-x-[-50%]
text-center outline-none border-[1px] focus:border-2 duration-200 pr-2 shadow-lg bg-white dark:bg-[rgb(23,25,29)]
dark:border-neutral-500 dark:focus:border-neutral-300 placeholder:text-slate-500
dark:placeholder:text-slate-400 text-slate-900 dark:text-white"
id="searchBox"
type="text"
placeholder="Search"
onFocus={props.onFocus}
/>
</div>
);
} else if (style == "image") {
return (
// 祖传样式,勿动
<div className="absolute w-full top-[8.5rem] lg:top-56 short:top-24 z-1 left-1/2 translate-x-[-50%] ">
<input
className={
`absolute z-1 w-2/3 sm:w-80 md:w-[400px] focus:w-11/12 focus:sm:w-[700px] hover:w-11/12
hover:sm:w-[700px] h-10 rounded-3xl left-1/2 translate-x-[-50%] text-center outline-none
border-solid border-0 duration-200 pr-2 shadow-lg` +
(settings.bgBlur
? `bg-[rgba(255,255,255,0.5)] dark:bg-[rgba(24,24,24,0.75)] backdrop-blur-xl
placeholder:text-slate-500 dark:placeholder:text-slate-400 text-slate-900 dark:text-white`
: `bg-[rgba(235,235,235,0.9)] dark:bg-[rgba(20,20,20,0.9)] placeholder:text-slate-500
text-slate-800 dark:text-white`)
}
id="searchBox"
type="text"
placeholder="Search"
onFocus={props.onFocus}
/>
</div>
);
}
}

View File

@ -1,10 +0,0 @@
import { atom, selector } from "recoil";
const bgFocusState = atom({
key: "isBackgroundFocus",
default: false
});
export {
bgFocusState,
}

View File

@ -1,14 +0,0 @@
import { atom, selector } from "recoil";
const settingsState = atom({
key: "settings",
default: {
version: 1,
elementBackdrop: true,
bgBlur: true
}
});
export {
settingsState,
}

48
components/time.tsx Normal file
View File

@ -0,0 +1,48 @@
"use client";
import { useState, useEffect } from "react";
export default function Time(props: {
showSecond: boolean
}) {
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(new Date());
}, 150);
return () => {
clearInterval(timer);
};
}, []);
const formatTime = () => {
const hours = currentTime.getHours().toString().padStart(2, "0");
const minutes = currentTime.getMinutes().toString().padStart(2, "0");
const seconds = currentTime.getSeconds().toString().padStart(2, "0");
if (props.showSecond) {
return `${hours}:${minutes}:${seconds}`;
} else {
return `${hours}:${minutes}`;
}
};
return (
<div
className="absolute top-20 lg:top-44 short:top-0 translate-x-[-50%]
left-1/2 w-11/12 sm:w-[700px] text:black
dark:text-white text-3xl text-left text-shadow-lg z-10"
>
{formatTime()}{" "}
<span className="text-lg leading-9 relative">
{new Intl.DateTimeFormat(navigator.language, {
year: "numeric",
month: "short",
day: "numeric"
}).format(currentTime)}
</span>
</div>
);
}

34
global.d.ts vendored
View File

@ -1,3 +1,31 @@
type settings = { import { Suggestion } from "search-engine-autocomplete";
bgBlur: boolean
}; interface settingsType extends object{
"version": number,
"elementBackdrop": boolean,
"bgBlur": boolean,
"timeShowSecond": boolean,
"currentSearchEngine": string,
"searchInNewTab": boolean,
"searchEngines": {
[key: string]: string
},
}
interface suggestionsResponse extends object{
suggestions: Suggestion[],
query: string,
verbatimRelevance: number,
time: number
}
type suggestionItem = {
suggestion: string,
type: string,
relativeRelevance?: number,
relevance: number,
prompt?: string | React.ReactElement,
intention?: string | null,
probability?: number,
confidence?: number,
}

5
i18n/ar.json Executable file
View File

@ -0,0 +1,5 @@
{
"search" : {
"placeholder" : "ابحث أو اكتب عنوان URL"
}
}

5
i18n/de.json Executable file
View File

@ -0,0 +1,5 @@
{
"search" : {
"placeholder" : "Suche oder gib eine URL ein"
}
}

5
i18n/en.json Executable file
View File

@ -0,0 +1,5 @@
{
"search" : {
"placeholder" : "Search or type a URL"
}
}

5
i18n/es.json Executable file
View File

@ -0,0 +1,5 @@
{
"search" : {
"placeholder" : "Buscar o escribir una URL"
}
}

5
i18n/fr.json Executable file
View File

@ -0,0 +1,5 @@
{
"search" : {
"placeholder" : "Rechercher ou saisir une URL"
}
}

5
i18n/it.json Executable file
View File

@ -0,0 +1,5 @@
{
"search" : {
"placeholder" : "Cerca o digita un URL"
}
}

5
i18n/ja.json Executable file
View File

@ -0,0 +1,5 @@
{
"search" : {
"placeholder" : "検索またはURLを入力"
}
}

5
i18n/ko.json Executable file
View File

@ -0,0 +1,5 @@
{
"search" : {
"placeholder" : "검색 또는 URL 입력"
}
}

5
i18n/pt.json Executable file
View File

@ -0,0 +1,5 @@
{
"search" : {
"placeholder" : "Pesquisar ou digitar uma URL"
}
}

5
i18n/ru.json Executable file
View File

@ -0,0 +1,5 @@
{
"search" : {
"placeholder" : "Искать или ввести URL"
}
}

5
i18n/zh.json Executable file
View File

@ -0,0 +1,5 @@
{
"search" : {
"placeholder" : "搜索或输入网址"
}
}

13
index.html Normal file
View File

@ -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>

8
lib/copy.ts Normal file
View File

@ -0,0 +1,8 @@
export default function copyToClipboard(value: string){
const textarea = document.createElement("textarea");
textarea.value = value;
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
}

18
lib/normalizeURL.ts Normal file
View File

@ -0,0 +1,18 @@
export function normalizeURL(input: string): string {
try {
// try to create a URL object
const url = new URL(input);
// if the URL is valid, return it
return url.href;
} catch (error) {
// if the URL is invalid, try to add the protocol
const withHTTP = "http://" + input;
try {
const urlWithHTTP = new URL(withHTTP);
return urlWithHTTP.href;
} catch (error) {
// if the URL is still invalid, return the original input
return input;
}
}
}

View File

@ -0,0 +1,25 @@
import { settingsType, suggestionItem } from "global";
import copyToClipboard from "lib/copy";
import { normalizeURL } from "lib/normalizeURL";
import search from "lib/search";
export default function (
index: number,
suggestion: suggestionItem[],
query: string,
settings: settingsType,
searchBoxRef: React.RefObject<HTMLInputElement>
) {
const selected = suggestion[index];
const engine = settings.searchEngines[settings.currentSearchEngine];
const newTab = settings.searchInNewTab;
if (selected.type === "QUERY" || selected.type === "default") {
search(selected.suggestion, engine, newTab);
} else if (selected.type === "NAVIGATION" || selected.type === "default-link") {
window.open(normalizeURL(selected.suggestion));
} else if (selected.type === "text") {
console.log("????");
copyToClipboard(selected.suggestion);
searchBoxRef.current?.focus();
}
}

4
lib/search.ts Normal file
View File

@ -0,0 +1,4 @@
export default function(query: string, engine: string, newTab: boolean = true) {
if(newTab) window.open(engine.replace("%s", query));
else window.location.href = engine.replace("%s", query);
}

5
lib/state/background.ts Normal file
View File

@ -0,0 +1,5 @@
import { atom } from "jotai";
const bgFocusAtom = atom(false);
export { bgFocusAtom };

5
lib/state/query.ts Normal file
View File

@ -0,0 +1,5 @@
import { atom } from "jotai";
const queryAtom = atom("");
export { queryAtom };

24
lib/state/settings.ts Normal file
View File

@ -0,0 +1,24 @@
import { settingsType } from "global";
import { atomWithStorage } from 'jotai/utils'
const defaultSettings: settingsType = {
"version": 2,
"elementBackdrop": true,
"bgBlur": true,
"timeShowSecond": false,
"currentSearchEngine": "google",
"searchInNewTab": true,
"searchEngines": {
"google": "https://www.google.com/search?q=%s",
"bing": "https://www.bing.com/search?q=%s",
"baidu": "https://www.baidu.com/s?wd=%s",
"duckduckgo": "https://duckduckgo.com/?q=%s",
"yandex": "https://yandex.com/search/?text=%s",
"yahoo": "https://search.yahoo.com/search?p=%s",
"ecosia": "https://www.ecosia.org/search?q=%s"
}
};
const settingsAtom = atomWithStorage('settings', defaultSettings);
export { settingsAtom };

6
lib/state/suggestion.ts Normal file
View File

@ -0,0 +1,6 @@
import { suggestionItem } from "global";
import { atom } from "jotai";
const suggestionAtom = atom([] as suggestionItem[]);
export { suggestionAtom };

View File

@ -0,0 +1,5 @@
import { atom } from "jotai";
const selectedSuggestionAtom = atom(0);
export { selectedSuggestionAtom };

View File

@ -1,15 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "a2x.pub",
port: "",
pathname: "/*"
}
]
}
};
export default nextConfig;

View File

@ -1,29 +1,44 @@
{ {
"name": "sparkhome", "name": "sparkhome",
"version": "4.1.0", "private": false,
"private": false, "version": "5.1.0",
"scripts": { "type": "module",
"dev": "next dev", "scripts": {
"build": "next build", "dev": "vite",
"start": "next start", "build": "tsc -b && vite build",
"lint": "next lint" "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
}, "preview": "vite preview"
"dependencies": { },
"next": "14.1.1", "dependencies": {
"react": "^18.2.0", "i18next": "^23.11.5",
"react-dom": "^18.2.0", "i18next-browser-languagedetector": "^8.0.0",
"recoil": "^0.7.7", "i18next-icu": "^2.3.0",
"valid-url": "^1.0.9", "jotai": "^2.8.3",
"validate-color": "^2.2.4" "react": "^18.3.1",
}, "react-dom": "^18.3.1",
"devDependencies": { "react-i18next": "^14.1.2",
"@types/node": "^20", "react-router": "^6.23.1",
"@types/react": "^18", "react-router-dom": "^6.23.1",
"@types/react-dom": "^18", "search-engine-autocomplete": "^0.4.3",
"@types/valid-url": "^1.0.7", "valid-url": "^1.0.9",
"autoprefixer": "^10.0.1", "validate-color": "^2.2.4"
"postcss": "^8", },
"tailwindcss": "^3.3.0", "devDependencies": {
"typescript": "^5" "@types/react": "^18.3.3",
} "@types/react-dom": "^18.3.0",
"@types/valid-url": "^1.0.7",
"@typescript-eslint/eslint-plugin": "^7.13.1",
"@typescript-eslint/parser": "^7.13.1",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.4",
"typescript": "^5.2.2",
"vite": "^5.3.1",
"vite-plugin-pages": "^0.32.2",
"vite-tsconfig-paths": "^4.3.2"
}
} }

19
pages/index.tsx Normal file
View File

@ -0,0 +1,19 @@
import Background from "components/background";
import Time from "components/time";
import { useAtomValue, useSetAtom } from "jotai";
import Search from "components/search";
import { settingsAtom } from "lib/state/settings";
import { bgFocusAtom } from "lib/state/background";
export default function Homepage() {
const settings = useAtomValue(settingsAtom);
const setBgFocus = useSetAtom(bgFocusAtom);
return (
<div>
<Background />
<Search onFocus={() => setBgFocus(true)} />
<Time showSecond={settings.timeShowSecond} />
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
module.exports = { export default {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {}
}, }
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 MiB

74
src/app.tsx Normal file
View File

@ -0,0 +1,74 @@
import { Suspense } from "react";
import { useRoutes } from "react-router-dom";
import routes from "~react-pages";
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from 'i18next-browser-languagedetector';
import ICU from 'i18next-icu';
import * as en from "i18n/en.json"
import * as zh from "i18n/zh.json"
import * as ja from "i18n/ja.json"
import * as ar from "i18n/ar.json"
import * as de from "i18n/de.json"
import * as es from "i18n/es.json"
import * as fr from "i18n/fr.json"
import * as it from "i18n/it.json"
import * as ko from "i18n/ko.json"
import * as pt from "i18n/pt.json"
import * as ru from "i18n/ru.json"
i18n.use(initReactI18next) // passes i18n down to react-i18next
.use(LanguageDetector)
.use(ICU)
.init({
resources: {
en: {
translation: en
},
zh: {
translation: zh
},
ja: {
translation: ja
},
ar: {
translation: ar
},
de: {
translation: de
},
es: {
translation: es
},
fr: {
translation: fr
},
it: {
translation: it
},
ko: {
translation: ko
},
pt: {
translation: pt
},
ru: {
translation: ru
}
},
fallbackLng: "en",
interpolation: {
escapeValue: false // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape
},
detection: {
order: ['navigator'],
caches: []
}
});
export function App() {
return <Suspense fallback={<p>Loading...</p>}>{useRoutes(routes)}</Suspense>;
}

3
src/index.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

15
src/main.tsx Normal file
View File

@ -0,0 +1,15 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { App } from "./app";
import "./index.css";
const app = createRoot(document.getElementById("root")!);
app.render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
);

2
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-pages/client-react" />

18
tailwind.config.js Normal file
View File

@ -0,0 +1,18 @@
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}"
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))"
}
}
}
};
export default config;

View File

@ -1,20 +0,0 @@
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
},
plugins: [],
};
export default config;

28
tsconfig.app.json Normal file
View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"baseUrl": ".",
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "**/*.ts", "**/*.tsx", "global.d.ts"]
}

View File

@ -1,26 +1,11 @@
{ {
"compilerOptions": { "files": [],
"lib": ["dom", "dom.iterable", "esnext"], "references": [
"allowJs": true, {
"skipLibCheck": true, "path": "./tsconfig.app.json"
"strict": true, },
"noEmit": true, {
"esModuleInterop": true, "path": "./tsconfig.node.json"
"module": "esnext", }
"moduleResolution": "bundler", ]
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "global.d.ts", "app/store/store.js"],
"exclude": ["node_modules"]
} }

13
tsconfig.node.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true
},
"include": ["vite.config.ts"]
}

15
vite.config.ts Normal file
View File

@ -0,0 +1,15 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import Pages from "vite-plugin-pages";
import tsconfigPaths from 'vite-tsconfig-paths';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
Pages({
dirs: "./pages/"
}),
tsconfigPaths()
]
});