From b77c1a129f5cdfd745f80ac68fe56281560bc6fe Mon Sep 17 00:00:00 2001 From: Folleach Date: Sun, 13 Oct 2024 16:54:02 +0500 Subject: [PATCH] implement roulette v2 --- .gitignore | 2 + src/App.tsx | 14 +- src/components/AdvanceInput/AdvanceInput.tsx | 109 +++++++++---- src/components/AdvanceInput/FilterView.tsx | 16 ++ src/components/AdvanceInput/RequestView.tsx | 15 ++ src/components/AdvanceInput/advanceInput.css | 20 +++ .../Roulette/ChallengeSettingsComponent.tsx | 72 +++++---- src/components/Roulette/Roulette.tsx | 10 +- src/components/Roulette/RouletteItem.tsx | 23 ++- src/components/Roulette/RouletteSlider.tsx | 8 +- .../SelectableItem/SelectableItem.tsx | 13 ++ src/components/Slider.tsx | 5 +- src/components/Tag/Tag.tsx | 13 ++ src/components/Tag/tag.css | 21 +++ src/components/TextField.tsx | 2 +- src/components/Weights/Weight.tsx | 13 ++ src/components/Weights/WeightsView.tsx | 39 +++++ src/context/RouletteContext.tsx | 40 +++-- src/defaultTheme.css | 6 +- src/index.css | 30 ++++ src/pages/MainPage.tsx | 4 +- .../Roulette/CreatingNewRouletteSubpage.tsx | 149 ++++++++---------- src/pages/Roulette/MainRouletteSubpage.tsx | 11 +- src/pages/RoulettePage.tsx | 49 +++--- src/pages/SearchPage.tsx | 18 ++- src/server/Backbone.ts | 2 +- src/services/roulette/models.ts | 7 +- src/services/search/QueryBuilder.ts | 72 ++++++--- src/services/search/SearchService.ts | 53 ++++++- src/services/search/models.ts | 17 +- tsconfig.json | 2 +- 31 files changed, 613 insertions(+), 242 deletions(-) create mode 100644 src/components/AdvanceInput/FilterView.tsx create mode 100644 src/components/AdvanceInput/RequestView.tsx create mode 100644 src/components/AdvanceInput/advanceInput.css create mode 100644 src/components/SelectableItem/SelectableItem.tsx create mode 100644 src/components/Tag/Tag.tsx create mode 100644 src/components/Tag/tag.css create mode 100644 src/components/Weights/Weight.tsx create mode 100644 src/components/Weights/WeightsView.tsx diff --git a/.gitignore b/.gitignore index 4d29575..eaf6799 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +/static/images/ diff --git a/src/App.tsx b/src/App.tsx index fd8170b..254ab31 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,13 +15,13 @@ const App: React.FC = () => { return - - - }> - }> - }> - - + + + }> + }> + }> + + ; diff --git a/src/components/AdvanceInput/AdvanceInput.tsx b/src/components/AdvanceInput/AdvanceInput.tsx index eddb029..0e2f409 100644 --- a/src/components/AdvanceInput/AdvanceInput.tsx +++ b/src/components/AdvanceInput/AdvanceInput.tsx @@ -1,46 +1,95 @@ import React, { useEffect, useState } from "react"; -import { Action } from "../../common/DotNetFeatures"; -import { IFilterDefinition, QueryRequest } from "../../services/search/models"; +import { AutocompleteResult, IFilterDefinition, QueryRequest } from "../../services/search/models"; import { useSearch } from "../../context/SearchContext"; -import { TextField } from "../TextField"; -import { buildQuery, parseQuery, parseRawQuery, toRaw } from "../../services/search/QueryBuilder"; +import { parseRawQuery, readable, toRaw, toRawFilter, toRawFilters } from "../../services/search/QueryBuilder"; +import './advanceInput.css'; +import SelectableItem from "../SelectableItem/SelectableItem"; +import FilterView from "./FilterView"; export interface IAdvanceInputParameters { prepend?: QueryRequest, - submit: Action + submit: (value: QueryRequest) => void + changed?: (value: QueryRequest) => void } -// grammar: (type + operator + value)* text -// length: 10 -// length > 10 ролововрвововоароар - export default function AdvanceInput(p: IAdvanceInputParameters) { - const [filters, setFilters] = useState(); + const [completions, setCompletions] = useState(); + const [focused, setFocused] = useState(false); + const state = p.prepend ?? { f: [] } const search = useSearch(); - const [state, setState] = useState(p.prepend ? toRaw(p.prepend) : ""); useEffect(() => { search - .findFilters() - .then(x => setFilters(x)) - .catch(e => console.error("failed to fetch builtin filters", e)); - }, []); + .autocomplete(state.t ?? "") + .then(x => setCompletions(x)) + .catch(e => { + console.error("failed to fetch completions", e); + setCompletions({ base: "", additional: [] }); + }); + }, [state]); + + function componentKeyProcess(e: React.KeyboardEvent) { + if (e.key === "Escape") { + setFocused(false); + } + } + + let input: HTMLInputElement | null = null; + return
+
input?.focus()}> +
+ {state && state.f.map(x => )} + + input = x} + onFocus={x => setFocused(true)} + value={state?.t ? state.t : ""} + onChange={event => { + const rawFilters = state ? toRawFilters(state) : ""; + const query = parseRawQuery((rawFilters ? `${rawFilters} ` : "") + event.target.value) + if (p.changed) { + p.changed(query) + } + if (!focused) + setFocused(true); + }} + onKeyDown={e => { + if (e.key === 'Enter') + { + const rawFilters = state ? toRawFilters(state) : ""; + const query = parseRawQuery((rawFilters ? `${rawFilters} ` : "") + (state.t ?? "") + (state.t?.lastIndexOf(" ") === state.t?.length ? " " : "")); + if (p.changed) + p.changed(query); + p.submit(query); + } + if (e.key === 'Backspace' && (state.t?.length ?? 0) == 0 && state.f.length > 0) { + const filter = state.f.pop(); + if (!filter || !p.changed) + return; + p.changed({ + f: state.f, + t: toRawFilter(filter) + }); + } + }}> + +
- let input: HTMLInputElement | null; - return
input?.focus()}> -
- input = x} - value={state} - onChange={event => { - setState(event.target.value) - }} - onKeyDown={e => { - if (e.key === 'Enter') - p.submit(parseRawQuery(state)); - }}> + {(completions?.additional?.length ?? 0) > 0 && focused && +
+
+ {completions?.additional?.map(x => { + if (p.changed) { + const rawFilters = state ? toRawFilters(state) : ""; + const query = parseRawQuery((rawFilters ? `${rawFilters} ` : "") + `${state.t ?? ""}${x} `); + p.changed(query); + } + input?.focus(); + }}>{completions.base}{x})} +
+
+ }
- {/* {filters?.map(x =>

{x.name}-{x.type}

)} */}
; } diff --git a/src/components/AdvanceInput/FilterView.tsx b/src/components/AdvanceInput/FilterView.tsx new file mode 100644 index 0000000..fb5be76 --- /dev/null +++ b/src/components/AdvanceInput/FilterView.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { Filter } from '../../services/search/models'; +import { readable } from '../../services/search/QueryBuilder'; +import './advanceInput.css'; + +export interface IFilterViewProps { + filter: Filter +} + +export default function FilterView(p: IFilterViewProps) { + return + {p.filter.n} + {readable(p.filter.o)} + {p.filter.v} + +} diff --git a/src/components/AdvanceInput/RequestView.tsx b/src/components/AdvanceInput/RequestView.tsx new file mode 100644 index 0000000..f9ddafb --- /dev/null +++ b/src/components/AdvanceInput/RequestView.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { QueryRequest } from "../../services/search/models"; +import FilterView from "./FilterView"; +import './advanceInput.css'; + +export interface IRequestViewProps { + request: QueryRequest +} + +export default function RequestView(p: IRequestViewProps) { + return
+ {p.request.f && p.request.f.map(x => )} + {p.request.t && {p.request.t}} +
+} diff --git a/src/components/AdvanceInput/advanceInput.css b/src/components/AdvanceInput/advanceInput.css new file mode 100644 index 0000000..b861ee3 --- /dev/null +++ b/src/components/AdvanceInput/advanceInput.css @@ -0,0 +1,20 @@ +.filter-highlight { + background: #e2f4ff; + color: #121d28; + border-radius: 0.5em; + padding: 0.5em 0.7em; +} + +input span { + outline: none; + border: none; + background: none; + min-height: 36px; + align-items: center; + font: inherit; + width: 100%; + padding: 0 12px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} diff --git a/src/components/Roulette/ChallengeSettingsComponent.tsx b/src/components/Roulette/ChallengeSettingsComponent.tsx index 13e1aab..3af53e1 100644 --- a/src/components/Roulette/ChallengeSettingsComponent.tsx +++ b/src/components/Roulette/ChallengeSettingsComponent.tsx @@ -1,48 +1,60 @@ -import React, {useEffect, useState} from "react"; -import { IDemonWeights } from "../../services/roulette/models"; +import React, { useEffect, useState } from "react"; import { get } from "../../server/Backbone"; import { Plate, PlateStyle } from "../Plate"; import Text, { TextStyle } from "../Text/Text"; import { RouletteSlider } from "./RouletteSlider"; - -interface IRouletteBalanceResponse { - weights: IDemonWeights -} +import { IBalance, IRouletteLevelWeights } from "../../context/RouletteContext"; +import { QueryRequest } from "../../services/search/models"; +import { buildQuery } from "../../services/search/QueryBuilder"; export interface IRouletteBalanceProps { - onChange: (value: IDemonWeights) => void + request: QueryRequest + onChange: (value: IBalance) => void } export const ChallengeSettingsComponent: React.FC = p => { - const [weights, setWeights] = useState(); + const [balance, setBalance] = useState({total: 0, weights: {}}); useEffect(() => { - get("roulette/balance").then(result => { - if (result?.weights) - setWeights(result.weights); - }).catch(e => console.log("error while fetching roulette balance")); - }, []); + get("roulette/v2/balance?" + new URLSearchParams({ query: buildQuery(p.request) })) + .then(result => { + if (result) + setBalance(result); + console.log("fetched", result); + }) + .catch(e => console.log("error while fetching roulette balance")); + }, [p.request]); useEffect(() => { - if (weights) - p.onChange(weights); - }, [weights]) + if (balance) + p.onChange(balance); + }, [balance]) + + function render(current: number | undefined, icon: string, update: (v: number, weight: IRouletteLevelWeights) => IRouletteLevelWeights) { + const filter: string | undefined = current != undefined ? undefined : "grayscale(0.9)"; + return
+ setBalance(prev => ({ ...prev!, weights: update(e, prev.weights) }))} /> +
+ } return - {weights &&
- Use the sliders below to adjust the likelihood of encountering different - types of demons - setWeights(prev => ({...prev!, easyDemon: e}))}/> - setWeights(prev => ({...prev!, mediumDemon: e}))}/> - setWeights(prev => ({...prev!, hardDemon: e}))}/> - setWeights(prev => ({...prev!, insaneDemon: e}))}/> - setWeights(prev => ({...prev!, extremeDemon: e}))}/> + {balance &&
+ Use the sliders below to adjust the likelihood of encountering different types of demons + {render(balance.weights.auto, `/images/difficulty_auto.png`, (e, prev) => ({ ...prev, auto: e }))} + {render(balance.weights.undef, `/images/difficulty_undefined.png`, (e, prev) => ({ ...prev!, undef: e }))} + + {render(balance.weights.easy, `/images/difficulty_easy.png`, (e, prev) => ({ ...prev!, easy: e }))} + {render(balance.weights.normal, `/images/difficulty_normal.png`, (e, prev) => ({ ...prev!, normal: e }))} + {render(balance.weights.hard, `/images/difficulty_hard.png`, (e, prev) => ({ ...prev!, hard: e }))} + {render(balance.weights.harder, `/images/difficulty_harder.png`, (e, prev) => ({ ...prev!, harder: e }))} + {render(balance.weights.insane, `/images/difficulty_insane.png`, (e, prev) => ({ ...prev!, insane: e }))} + + {render(balance.weights.easyDemon, `/images/difficulty_demon_easy.png`, (e, prev) => ({ ...prev!, easyDemon: e }))} + {render(balance.weights.mediumDemon, `/images/difficulty_demon_normal.png`, (e, prev) => ({ ...prev!, mediumDemon: e }))} + {render(balance.weights.hardDemon, `/images/difficulty_demon_hard.png`, (e, prev) => ({ ...prev!, hardDemon: e }))} + {render(balance.weights.insaneDemon, `/images/difficulty_demon_harder.png`, (e, prev) => ({ ...prev!, insaneDemon: e }))} + {render(balance.weights.extremeDemon, `/images/difficulty_demon_insane.png`, (e, prev) => ({ ...prev!, extremeDemon: e }))}
} - {!weights && Loading...} + {!balance && Loading...} } diff --git a/src/components/Roulette/Roulette.tsx b/src/components/Roulette/Roulette.tsx index 8a0af3b..d3791e3 100644 --- a/src/components/Roulette/Roulette.tsx +++ b/src/components/Roulette/Roulette.tsx @@ -1,4 +1,4 @@ -import React, { CSSProperties } from 'react'; +import React, { CSSProperties, useEffect, useState } from 'react'; import { HighlightedString } from '../../services/models'; export interface IRouletteSegment { @@ -23,7 +23,6 @@ const Roulette: React.FC = p => { const randSeconds = 5 + Math.random() * 3; const rand90 = Math.floor(Math.random() * 90); const t = `translate(120, 120) rotate(${rand360}deg)`; - console.log(t) const style: CSSProperties = { transform: `translate(1200px, 1200px) rotate(${rand360}deg)`, animation: 'roulette_spin', @@ -31,7 +30,8 @@ const Roulette: React.FC = p => { animationDuration: `${randSeconds}s`, animationFillMode: 'forwards' }; - const content = + + const content =
-
- {p.roulette.type === "challenge" ? "Challenge" : "Normal"} + {!p.roulette.request && +
+ {p.roulette.type === "challenge" ? "Challenge" : "Normal"} +
+ } + {p.roulette.request && +
+ +
+ } + {p.roulette.weights && +
+ +
+ } +
+ {new Date(p.roulette.createDt).toLocaleString()}
- {new Date(p.roulette.createDt).toLocaleString()}
} diff --git a/src/components/Roulette/RouletteSlider.tsx b/src/components/Roulette/RouletteSlider.tsx index 1c08027..4f8a1ff 100644 --- a/src/components/Roulette/RouletteSlider.tsx +++ b/src/components/Roulette/RouletteSlider.tsx @@ -4,19 +4,17 @@ import Text, { TextStyle } from "../Text/Text"; export interface IRouletteSliderProps { icon: string, - init: number, + init?: number | undefined, onChange?: (value: number) => void } export const RouletteSlider: React.FC = (p) => { - const [value, setValue] = useState(p.init); return
- { - setValue(e); + { if (p.onChange) p.onChange(e); }}/> - {value} + {p.init ?? 0}
} diff --git a/src/components/SelectableItem/SelectableItem.tsx b/src/components/SelectableItem/SelectableItem.tsx new file mode 100644 index 0000000..3037018 --- /dev/null +++ b/src/components/SelectableItem/SelectableItem.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { IHaveChild } from "../../common/IHaveChild"; + +export interface ISelectableItemProps extends IHaveChild { + action: () => void +} + +export default function SelectableItem(p: ISelectableItemProps) { + return

{ + if (e.key === "Enter") + p.action(); + }} onClick={p.action}>{p.children}

+} diff --git a/src/components/Slider.tsx b/src/components/Slider.tsx index 935d644..ce4d1ff 100644 --- a/src/components/Slider.tsx +++ b/src/components/Slider.tsx @@ -5,14 +5,13 @@ export interface ISliderProps { min: number, max: number, step: number, + disabled?: boolean | false, onChange?: (value: number) => void } export const Slider: React.FC = p => { - const [value, setValue] = useState(p.init); return
- { - setValue(Number(e.target.value)); + { if (p.onChange) p.onChange(Number(e.target.value)) }}/> diff --git a/src/components/Tag/Tag.tsx b/src/components/Tag/Tag.tsx new file mode 100644 index 0000000..aa4a992 --- /dev/null +++ b/src/components/Tag/Tag.tsx @@ -0,0 +1,13 @@ +import React from 'react' +import './tag.css'; + +export interface ITagProps { + text: string, + onClick: () => void +} + +export default function Tag(p: ITagProps) { + return
+ {p.text} +
; +} \ No newline at end of file diff --git a/src/components/Tag/tag.css b/src/components/Tag/tag.css new file mode 100644 index 0000000..b76eb6b --- /dev/null +++ b/src/components/Tag/tag.css @@ -0,0 +1,21 @@ +.tag { + margin-right: 8px; + border-radius: 6px; + background-color: rgb(236, 238, 241); + color: #111; + padding-left: 12px; + padding-right: 12px; + padding-top: 4px; + padding-bottom: 6px; + vertical-align: middle; + user-select: none; + cursor: pointer; +} + +.tag:hover { + background-color: #85d2ff; +} + +.tag:active { + background-color: #a9dfff; +} diff --git a/src/components/TextField.tsx b/src/components/TextField.tsx index 4132c34..452aa70 100644 --- a/src/components/TextField.tsx +++ b/src/components/TextField.tsx @@ -24,7 +24,7 @@ const TextField: React.FC = p => { p.onChange(event.target.value) setState(event.target.value) }} - onKeyPress={e => { + onKeyDown={e => { if (p.filter && !p.filter(state)) return; p.apply && e.key === 'Enter' && p.apply(state) diff --git a/src/components/Weights/Weight.tsx b/src/components/Weights/Weight.tsx new file mode 100644 index 0000000..8637add --- /dev/null +++ b/src/components/Weights/Weight.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export interface IWeightProps { + value: number, + color: string, + icon: string +} + +export default function Weight(p: IWeightProps) { + return
+ +
+} diff --git a/src/components/Weights/WeightsView.tsx b/src/components/Weights/WeightsView.tsx new file mode 100644 index 0000000..a0068a0 --- /dev/null +++ b/src/components/Weights/WeightsView.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { IRouletteLevelWeights } from "../../context/RouletteContext"; +import Weight from "./Weight"; + +export interface IWeightsViewProps { + weights: IRouletteLevelWeights +} + +export default function WeightsView(p: IWeightsViewProps) { + return
+ {p.weights.undef != undefined && } + {p.weights.auto != undefined && } + + {p.weights.easy != undefined && } + {p.weights.normal != undefined && } + {p.weights.hard != undefined && } + {p.weights.harder != undefined && } + {p.weights.insane != undefined && } + + {p.weights.easyDemon != undefined && } + {p.weights.mediumDemon != undefined && } + {p.weights.hardDemon != undefined && } + {p.weights.insaneDemon != undefined && } + {p.weights.extremeDemon != undefined && } +
+} diff --git a/src/context/RouletteContext.tsx b/src/context/RouletteContext.tsx index ca056bd..ea2aadf 100644 --- a/src/context/RouletteContext.tsx +++ b/src/context/RouletteContext.tsx @@ -3,7 +3,7 @@ import React, { useContext, useEffect, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { get, post } from '../server/Backbone'; import { IHaveChild } from '../common/IHaveChild'; -import { SearchItem } from '../services/search/models'; +import { QueryRequest, SearchItem } from '../services/search/models'; enum RouletteState { Main, @@ -46,19 +46,31 @@ interface IRouletteSession { progress: IProgress[] } -export interface IDemonWeights { - easyDemon: number, - mediumDemon: number, - hardDemon: number, - insaneDemon: number, - extremeDemon: number +export interface IBalance { + weights: IRouletteLevelWeights, + total: number +} + +export interface IRouletteLevelWeights { + auto?: number | undefined, + undef?: number | undefined, + easy?: number | undefined, + normal?: number | undefined, + hard?: number | undefined, + harder?: number | undefined, + insane?: number | undefined, + easyDemon?: number | undefined, + mediumDemon?: number | undefined, + hardDemon?: number | undefined, + insaneDemon?: number | undefined, + extremeDemon?: number | undefined, } interface ICreateSessionRequest { type: string, name: string, - server: string, - weights: IDemonWeights + weights: IRouletteLevelWeights, + request: QueryRequest } interface IRouletteContext { @@ -71,7 +83,7 @@ interface IRouletteContext { // startNew(): void, open(id: string): void, goHome(): void, - create(name: string, type: string, server: string, weights: IDemonWeights): Promise, + create(name: string, weights: IRouletteLevelWeights, request: QueryRequest): Promise, getRoulette(): IRoulette, setPublished(id: string): void, addRouletteToMe(): void @@ -307,12 +319,12 @@ const RouletteProvider: React.FC = (p) => { }); } - const create = async (name: string, type: string, server: string, weights: IDemonWeights): Promise => { + const create = async (name: string, weights: IRouletteLevelWeights, request: QueryRequest): Promise => { return await post('roulette', { name, - type, - server, - weights + type: "Advance", + weights, + request }, { "sessionId": getSession() }) diff --git a/src/defaultTheme.css b/src/defaultTheme.css index d836c80..229e0a3 100644 --- a/src/defaultTheme.css +++ b/src/defaultTheme.css @@ -23,7 +23,7 @@ } .transitioned { - transition: ease all .2s; + transition: ease all .15s; } .shadowed { @@ -137,3 +137,7 @@ input { .border-red:focus-within { border: 2px solid #FF9EA2; } + +.ppad { + padding: 1em; +} diff --git a/src/index.css b/src/index.css index 180b74d..d275b54 100644 --- a/src/index.css +++ b/src/index.css @@ -23,3 +23,33 @@ code { color: #0f1f38; } +.roulette-gradient-text { + background-color: #000; + background-image: linear-gradient(0deg, rgba(255,113,66,1) 0%, rgba(255,237,91,1) 100%); + font-weight: 600; + font-size: 3em; + background-size: 100%; + background-clip: text; + -webkit-background-clip: text; + -moz-background-clip: text; + -webkit-text-fill-color: transparent; + -moz-text-fill-color: transparent; +} + +.roulette-gradient-text2 { + background-color: #000; + background-image: linear-gradient(0deg, rgba(255,216,203,1) 0%, rgba(255,248,189,1) 100%); + font-size: 3em; + font-weight: 900; + background-size: 100%; + background-clip: text; + -webkit-background-clip: text; + -moz-background-clip: text; + -webkit-text-fill-color: transparent; + -moz-text-fill-color: transparent; + text-shadow: + -0.05em -0.05em 0 rgba(255,216,203,1), + 0.05em -0.05em 0 rgba(255,216,203,1), + -0.05em 0.05em 0 rgba(255,216,203,1), + 0.05em 0.05em 0 rgba(255,216,203,1); +} diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index 59fcb11..c38cdba 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -8,15 +8,17 @@ import { ButtonSquare, ButtonSquareStyle } from '../components/ButtonSquare'; import RouletteSvg from '../svgs/roulette.svg'; import GithubMark from '../svgs/github-mark.svg'; import Text, { TextStyle } from '../components/Text/Text'; +import { QueryRequest } from '../services/search/models'; const MainPage: React.FC = () => { const navigate = useNavigate(); + const [state, setState] = useState({ f: [] }); return
- navigate("/search?query=" + buildQuery(x))}/> + navigate("/search?query=" + buildQuery(x))}/>
diff --git a/src/pages/Roulette/CreatingNewRouletteSubpage.tsx b/src/pages/Roulette/CreatingNewRouletteSubpage.tsx index 10bb4fb..9433001 100644 --- a/src/pages/Roulette/CreatingNewRouletteSubpage.tsx +++ b/src/pages/Roulette/CreatingNewRouletteSubpage.tsx @@ -1,117 +1,106 @@ -import React, {useState} from 'react'; -import {Button, ButtonStyle} from '../../components/Button'; -import { useRoulette } from '../../context/RouletteContext'; +import React, { useState } from 'react'; +import { Button, ButtonStyle } from '../../components/Button'; +import { IBalance, IRouletteLevelWeights, useRoulette } from '../../context/RouletteContext'; import { Plate, PlateStyle } from '../../components/Plate'; import Text, { TextStyle } from '../../components/Text/Text'; import { TextField } from '../../components/TextField'; import { ChallengeSettingsComponent } from '../../components/Roulette/ChallengeSettingsComponent'; +import { QueryRequest } from '../../services/search/models'; +import AdvanceInput from '../../components/AdvanceInput/AdvanceInput'; +import BadgeComponent from '../../components/Badge/BadgeComponent'; +import Tag from '../../components/Tag/Tag'; +import { buildQuery, parseQuery, parseRawQuery } from '../../services/search/QueryBuilder'; +import { useNavigate } from 'react-router-dom'; interface ICreatingProps { } +interface IPreparedRoulette { + name: string, + query: string +} + +const prepared: IPreparedRoulette[] = [ + { + name: "demons", + query: "difficulty is demons " + }, + { + name: "pointcreate list", + query: "list is pointcreate " + }, + { + name: "shitty", + query: "list is shitty " + }, + { + name: "impossible list", + query: "list is impossible " + }, + { + name: "auto", + query: "difficulty is auto " + }, + { + name: "clear", + query: "" + } +]; + export const CreatingNewRouletteSubpage: React.FC = (p) => { + const navigate = useNavigate(); const roulette = useRoulette(); - const [name, setName] = useState(""); - const [type, setType] = useState("default"); - const [server, setServer] = useState("geometrydash"); - const [warn, setWarn] = useState(false); + const [name, setName] = useState("untitled"); + const [request, setRequest] = useState(parseRawQuery(prepared.find(x => true)?.query)); + const [balance, setBalance] = useState(); const [creatingStatus, setCreatingStatus] = useState(0); - const [weights, setWeights] = useState({ - easyDemon: 0.3, - mediumDemon: 0.3, - hardDemon: 0.3, - insaneDemon: 0.3, - extremeDemon: 0.3 - }) const validateName = /^[a-zA-Z0-9 -]+$/; + const warn = !validateName.test(name); return
Create a new Roulette - {warn && You should specify roulette name using latin alphabet and numbers} - { - const valid = validateName.test(e); - setName(e); - if (valid) { - setWarn(false); - } - else - setWarn(true); - }} filter={e => validateName.test(e)}> - - {type !== "impossible_list" && type !== "auto" && type !== "shitty" && - Select Game Server -
-
-
} - - - Select your Game Mode -
-
-
+ {warn && You should specify roulette name using latin alphabet and numbers} + validateName.test(e)}> +
+ setRequest(request)} changed={x => setRequest(x)}/> + +
+

Search

+

{balance?.total ?? ""}

+
+
+ {/*
- {type === "default" && - In Normal mode, a random level is selected from all available levels each time you play. - This mode is great for a varied and unpredictable experience. Good luck! - } - {type === "challenge" && - In Challenge mode, you can set the probability of encountering different difficulty levels. - Once you've set your probabilities, the game will randomly select a difficulty based on those settings. And then a level will be selected that matches the current difficulty. Challenge mode is perfect for players who want to fine-tune their experience and test their skills against a specific level of difficulty. Good luck! -
- By default, the balance is set to be similar to the Normal mode. -
-
} - {type === "impossible_list" && - In the Impossible List mode, you will only come across levels from the site https://www.impossible-list.com/ - } - {type === "auto" && - Ha-ha, can you pass this INCREDIBLY difficult roulette of AUTO levels? - } - {type === "shitty" && - Abandon the hope of all those who enter here - } +
+ {prepared.map(x => setRequest(parseRawQuery(x.query))}/>)} +
- {type === "challenge" && setWeights(e)}/>} + {creatingStatus != 1 && <> -
- {creatingStatus == 2 && An error occurred during the creation of the roulette :(} +
+ {creatingStatus == 2 && An error occurred during the creation of the roulette :(}
-
+
} - {creatingStatus == 1 &&
+ {creatingStatus == 1 &&
Creating a new roulette...
}
diff --git a/src/pages/Roulette/MainRouletteSubpage.tsx b/src/pages/Roulette/MainRouletteSubpage.tsx index 2749642..17f2534 100644 --- a/src/pages/Roulette/MainRouletteSubpage.tsx +++ b/src/pages/Roulette/MainRouletteSubpage.tsx @@ -20,7 +20,13 @@ export const MainRouletteSubpage: React.FC = () => { useEffect(() => { get("roulette/sessions", { 'sessionId': `${localStorage.getItem("roulette/sessionId")}` - }).then(x => x ? setRoulettes(x) : console.error('can not get list of roulettes')); + }) + .then(x => { + if (!x) + return; + x.sort((a, b) => a.createDt > b.createDt ? -1 : 1); + setRoulettes(x); + }); }, []); const left: CSSProperties = { @@ -41,10 +47,9 @@ export const MainRouletteSubpage: React.FC = () => { +
{roulettes && roulettes.map(x => )}
- - } diff --git a/src/pages/RoulettePage.tsx b/src/pages/RoulettePage.tsx index 581d81b..7582390 100644 --- a/src/pages/RoulettePage.tsx +++ b/src/pages/RoulettePage.tsx @@ -1,5 +1,5 @@ -import React, {useState} from 'react'; -import {useNavigate} from 'react-router-dom'; +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { RouletteState, useRoulette } from '../context/RouletteContext'; import { IRouletteSegment, Roulette } from '../components/Roulette/Roulette'; import { getDemonDifficultyImage } from '../common/LevelHelper'; @@ -15,13 +15,8 @@ import { FinishRouletteSubpage } from './Roulette/FinishedRouletteSubpage'; import { NotFoundSubpage } from './Roulette/NotFoundSubpage'; import { PrepareToStartSubpage } from './Roulette/PrepareToStartSubpage'; -enum ViewRouletteState { - Rolling, - Idle -} - const RoulettePage: React.FC = () => { - const [viewState, setViewState] = useState(ViewRouletteState.Rolling); + const [viewState, setViewState] = useState(1); console.debug("VIEW STATE: " + viewState); const roulette = useRoulette(); @@ -59,34 +54,40 @@ const RoulettePage: React.FC = () => { }; const tryRolling = () => { - if (viewState == ViewRouletteState.Rolling) { - let rollManager = {"stop": false}; - return <> - setViewState(ViewRouletteState.Idle)}> - - - } + } function playing() { return (
- Roulette "{roulette.getRoulette().name}" - {viewState == ViewRouletteState.Rolling && tryRolling()} - {viewState == ViewRouletteState.Idle && <> +
+
+

{roulette.getRoulette().name}

+
+
+

{roulette.getRoulette().name}

+
+
+
+ {tryRolling()} +
+ {<> Number(e) > (roulette.getProgress(0, true)?.progress ?? 0) && Number(e) <= 100} apply={e => { roulette.setNextProgress(Number(e)); - setViewState(ViewRouletteState.Rolling); - }}>{`${(roulette.getProgress(0, true)?.progress ?? 0) + 1}`} + setViewState(state + 1); + }}>{`${(roulette.getProgress(0, true)?.progress ?? 0) + 1}`} @@ -97,7 +98,7 @@ const RoulettePage: React.FC = () => { + >
; return res; })} diff --git a/src/pages/SearchPage.tsx b/src/pages/SearchPage.tsx index ecb0db8..3d374b3 100644 --- a/src/pages/SearchPage.tsx +++ b/src/pages/SearchPage.tsx @@ -1,7 +1,7 @@ import React, {useEffect, useState} from 'react'; import {useNavigate, useSearchParams} from 'react-router-dom'; import AdvanceInput from '../components/AdvanceInput/AdvanceInput'; -import { SearchItem, SearchResult } from '../services/search/models'; +import { QueryRequest, SearchItem, SearchResult } from '../services/search/models'; import { useSearch } from '../context/SearchContext'; import { buildQuery, parseQuery } from '../services/search/QueryBuilder'; import Level from '../components/Level'; @@ -21,6 +21,7 @@ const SearchPage: React.FC = () => { isLucky: searchParams.get("lucky") === "1", page: Number.parseInt(searchParams.get("page") ?? "0") }); + const [input, setInput] = useState(state.query); const [result, setResult] = useState(); useEffect(() => { @@ -47,17 +48,18 @@ const SearchPage: React.FC = () => { return
-
+
} + icon={} onClick={e => navigate('/')} /> - setState({ - isLucky: state.isLucky, - page: state.page, - query: x - })} prepend={state.query} /> + setState({ + isLucky: state.isLucky, + page: state.page, + query: x + })} prepend={input} />
diff --git a/src/server/Backbone.ts b/src/server/Backbone.ts index fab913f..e6c2247 100644 --- a/src/server/Backbone.ts +++ b/src/server/Backbone.ts @@ -1,6 +1,6 @@ const { NODE_ENV } = process.env; -const GEOMETRY_BASE_URL = NODE_ENV === "development" ? "http://localhost:8080/api" : "https://geometrydash.ru/api"; +const GEOMETRY_BASE_URL = NODE_ENV === "development" ? "http://localhost:9090/api" : "https://geometrydash.ru/api"; console.info(`backbone base url: ${GEOMETRY_BASE_URL}`); if (!GEOMETRY_BASE_URL) throw new Error("base url 'GEOMETRY_BASE_URL' is not set"); diff --git a/src/services/roulette/models.ts b/src/services/roulette/models.ts index d190bad..87a5879 100644 --- a/src/services/roulette/models.ts +++ b/src/services/roulette/models.ts @@ -1,10 +1,15 @@ +import { IRouletteLevelWeights } from "../../context/RouletteContext" +import { QueryRequest } from "../search/models" + export interface IRoulettePreview { id: string, name?: string, type: string, owner: boolean, isPublic: boolean, - createDt: Date + createDt: Date, + request?: QueryRequest | null, + weights?: IRouletteLevelWeights | null } export interface IDemonWeights { diff --git a/src/services/search/QueryBuilder.ts b/src/services/search/QueryBuilder.ts index 33d66d7..1475456 100644 --- a/src/services/search/QueryBuilder.ts +++ b/src/services/search/QueryBuilder.ts @@ -1,10 +1,29 @@ -import { Filter, FilterOperator, QueryRequest } from "./models"; +import { Filter, FilterOperator, PartialFilter, QueryRequest } from "./models"; +const filterRegex: RegExp = /((\S+)\s+(is|not|more than|less than)\s+(\S+)) {1}/g; +const partialFilterRegex: RegExp = /(\S+)\s+(?:(is|not|more than|less than)\s+(\S*))?/; const operationsNameMap = new Map(); operationsNameMap.set(FilterOperator.Equals, "is"); +operationsNameMap.set(FilterOperator.Equals | FilterOperator.Not, "not"); operationsNameMap.set(FilterOperator.More, "more than"); operationsNameMap.set(FilterOperator.Less, "less than"); +function getOperator(value: string): FilterOperator { + if (value == "is") + return FilterOperator.Equals; + if (value == "not") + return FilterOperator.Equals | FilterOperator.Not; + if (value == "more than") + return FilterOperator.More; + if (value == "less than") + return FilterOperator.Less; + return FilterOperator.Equals; +} + +export function readable(operator: FilterOperator) { + return operationsNameMap.get(operator); +} + export function buildQuery(request: QueryRequest): string { const json = JSON.stringify(request); const base64 = Buffer.from(json).toString('base64'); @@ -16,51 +35,64 @@ export function toRaw(request: QueryRequest): string { for (let filter of request.f) { if (query.length > 0) query += " "; - query += `${filter.n} ${operationsNameMap.get(filter.o)} ${filter.v}`; + query += toRawFilter(filter); } if (request.t) { if (query.length > 0) query += " "; - query += ` ${request.t}`; + query += `${request.t}`; } return query; } -// heh: l dada > 2 kek lol +export function toRawFilters(request: QueryRequest): string { + let query = ""; + for (let filter of request.f) { + if (query.length > 0) + query += " "; + query += toRawFilter(filter); + } + + return query; +} + +export function toRawFilter(filter: Filter): string { + return `${filter.n} ${readable(filter.o)} ${filter.v}`; +} export function parseRawQuery(value?: string): QueryRequest { if (!value) return { f: [] }; - let regex: RegExp = /((\S+)\s*(is|more than|less than)\s*(\S+))/g; - let matches = Array.from(value.matchAll(regex), m => m); + let matches = Array.from(value.matchAll(filterRegex), m => m); let filters: Filter[] = matches.map(x => { - - let operator: FilterOperator = FilterOperator.Equals; - if (x[3] == "is") - operator = FilterOperator.Equals; - if (x[3] == "more than") - operator = FilterOperator.More; - if (x[3] == "less than") - operator = FilterOperator.Less; - - let filter: Filter = { + return { n: x[2], - o: operator, + o: getOperator(x[3]), v: x[4] - } - return filter + }; }); const last = matches[matches.length - 1]; if (!last) return { f: filters, t: value }; const index = last.index + last[0].length; - const text = index < value.length ? value.substring(index).trim() : undefined; + const text = index < value.length ? value.substring(index) : undefined; console.log(matches); return { f: filters, t: text }; } +export function parsePartialFilter(input: string): PartialFilter | undefined { + const match = input.match(partialFilterRegex); + if (!match) + return undefined; + return { + n: match[1] ?? "", + o: match[2] ? getOperator(match[2]) : undefined, + v: match[3] + } +} + export function parseQuery(value: string | undefined): QueryRequest { if (value === undefined) return { f: [] }; diff --git a/src/services/search/SearchService.ts b/src/services/search/SearchService.ts index 3c883e6..de6eeb7 100644 --- a/src/services/search/SearchService.ts +++ b/src/services/search/SearchService.ts @@ -1,10 +1,11 @@ import { get } from "../../server/Backbone"; -import { IFilterDefinition, ISearchFilterResponse, QueryRequest, SearchResult } from "./models"; -import { buildQuery } from "./QueryBuilder"; +import { AutocompleteResult, FilterType, IFilterDefinition, ISearchFilterResponse, QueryRequest, SearchResult } from "./models"; +import { buildQuery, parsePartialFilter } from "./QueryBuilder"; export interface ISearchService { search: (reqeust: QueryRequest, isLucky: boolean, page: number) => Promise findFilters: () => Promise + autocomplete: (input: string) => Promise } export class SearchService implements ISearchService { @@ -28,4 +29,52 @@ export class SearchService implements ISearchService { } return Promise.reject(); } + + async autocomplete(input: string): Promise { + let filters = this.filters; + if (!filters) + filters = await this.findFilters(); + if (!filters) + return Promise.reject(); + + const partial = parsePartialFilter(input); + if (!partial) { + const result = filters + .filter(x => x.name.startsWith(input)) + .map(x => x.name.substring(input.length)); + return { + base: input, + additional: result + }; + } + if (partial.v === undefined) { + const filter = filters.find(x => x.name == partial.n); + const operator = input.substring(partial.n.length).trim(); + if (filter?.type == FilterType.Term) + return { base: operator ?? "", additional: this.trimByEnd([ "is", "not" ], operator ?? "") } + if (filter?.type == FilterType.Number) + return { base: operator ?? "", additional: this.trimByEnd([ "is", "not", "more than", "less than" ], operator ?? "") } + return Promise.reject(); + } + + let result = await get("search/completions?" + new URLSearchParams({ + filter: partial.n, + query: partial.v + })); + return result ?? Promise.reject(); + } + + private trimByEnd(values: string[], trimBy: string): string[] { + if (trimBy.length === 0) + return values; + return values.map(value => { + for (let i = trimBy.length; i > 0; i--) { + const suffix = trimBy.slice(trimBy.length - i); + if (value.startsWith(suffix)) + return value.slice(suffix.length); + } + + return undefined; + }).filter(x => x != undefined); + } } diff --git a/src/services/search/models.ts b/src/services/search/models.ts index 3627926..f5d132a 100644 --- a/src/services/search/models.ts +++ b/src/services/search/models.ts @@ -4,8 +4,8 @@ import { HighlightedString, LengthType } from "../models" export const enum FilterType { - Term, - Number + Term, // is, not + Number // more than, less than, is, not } export interface IFilterDefinition { @@ -33,6 +33,12 @@ export interface Filter { o: FilterOperator } +export interface PartialFilter { + n: string, + o?: FilterOperator | undefined + v?: string | undefined, +} + export interface QueryRequest { t?: string | undefined, f: Filter[] @@ -70,3 +76,10 @@ export interface SearchItem extends PreviewEntity { likes: number, length: LengthType, } + +// autocomplete + +export interface AutocompleteResult { + base: string, + additional: string[] +} diff --git a/tsconfig.json b/tsconfig.json index 3192086..daaf0e6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES5", + "target": "ES2015", "allowJs": true, "jsx": "react", "moduleResolution": "node",