mirror of
https://github.com/Radiquum/AniX.git
synced 2025-04-06 00:04:39 +00:00
feat(deploy): allow deploying on deta.space
- **BREAKING CHANGE**: Api url now /api/v1 \n **Fix**: Frontend build. \n **Fix**: errors about unknown styles BREAKING CHANGE:
This commit is contained in:
parent
d85ce45989
commit
d97ad7dbfe
19 changed files with 504 additions and 412 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -7,8 +7,12 @@ venv
|
|||
.VSCode
|
||||
*.code-workspace
|
||||
|
||||
# DetaSpace
|
||||
.space
|
||||
|
||||
# NextJS
|
||||
## dependencies
|
||||
standalone
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
|
57
.spaceignore
Normal file
57
.spaceignore
Normal file
|
@ -0,0 +1,57 @@
|
|||
# Python
|
||||
__pycache__
|
||||
venv
|
||||
.mypy_cache
|
||||
|
||||
# VSCode
|
||||
.VSCode
|
||||
*.code-workspace
|
||||
|
||||
# NextJS
|
||||
## dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
## testing
|
||||
coverage
|
||||
|
||||
## next.js
|
||||
.next
|
||||
out
|
||||
|
||||
## production
|
||||
build
|
||||
|
||||
## misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
## debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
## local env files
|
||||
.env*.local
|
||||
|
||||
## vercel
|
||||
.vercel
|
||||
|
||||
## typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# traefik
|
||||
traefik
|
||||
|
||||
# OtherFiles
|
||||
CHANGELOG.md
|
||||
docker*
|
||||
LICENSE
|
||||
README.md
|
||||
TODO.md
|
||||
.cz.yaml
|
||||
.pre-commit-config.yaml
|
||||
docs
|
|
@ -118,13 +118,13 @@ To access the docker logs you can use `docker compose -f docker-compose.dev.yml
|
|||
|
||||
## Deployment
|
||||
|
||||
### Docker Deployment
|
||||
<!-- ### Docker Deployment
|
||||
|
||||
To be added soon . . .
|
||||
To be added soon . . . -->
|
||||
|
||||
### Deta Space
|
||||
<!-- ### Deta Space
|
||||
|
||||
To be added soon . . .
|
||||
To be added soon . . . -->
|
||||
|
||||
<!-- ### Standalone
|
||||
|
||||
|
|
14
Spacefile
Normal file
14
Spacefile
Normal file
|
@ -0,0 +1,14 @@
|
|||
v: 0
|
||||
# icon: ./icon.png
|
||||
micros:
|
||||
- name: anix-api
|
||||
src: ./backend/
|
||||
engine: python3.9
|
||||
path: api
|
||||
run: uvicorn main:app --root-path /api
|
||||
dev: uvicorn main:app --reload
|
||||
|
||||
- name: anix-app
|
||||
src: ./frontend/
|
||||
engine: next
|
||||
primary: true
|
2
TODO.md
2
TODO.md
|
@ -1,5 +1,7 @@
|
|||
# TODO
|
||||
|
||||
- [ ] Add docker deployment
|
||||
|
||||
## Релизы
|
||||
|
||||
- [ ] Авто-ген ссылка с именем
|
||||
|
|
|
@ -36,26 +36,28 @@ TAGS = [
|
|||
},
|
||||
]
|
||||
|
||||
PREFIX = "/v1"
|
||||
|
||||
app = FastAPI(
|
||||
openapi_tags=TAGS,
|
||||
title="AniX API",
|
||||
description="unofficial API proxy for Anixart android application.",
|
||||
openapi_url="/api/openapi.json",
|
||||
docs_url="/api/docs",
|
||||
openapi_url=f"{PREFIX}/openapi.json",
|
||||
docs_url=f"{PREFIX}/docs",
|
||||
redoc_url=None,
|
||||
)
|
||||
|
||||
app.include_router(profile.router, prefix="/api/profile", tags=["Profile"])
|
||||
app.include_router(auth.router, prefix="/api/auth", tags=["Profile"])
|
||||
app.include_router(profile.router, prefix=f"{PREFIX}/profile", tags=["Profile"])
|
||||
app.include_router(auth.router, prefix=f"{PREFIX}/auth", tags=["Profile"])
|
||||
|
||||
app.include_router(release.router, prefix="/api/release", tags=["Releases"])
|
||||
app.include_router(release.router, prefix=f"{PREFIX}/release", tags=["Releases"])
|
||||
|
||||
app.include_router(index.router, prefix="/api/index", tags=["Index"])
|
||||
app.include_router(bookmarks.router, prefix="/api/bookmarks", tags=["Bookmarks"])
|
||||
app.include_router(favorites.router, prefix="/api/favorites", tags=["Favorites"])
|
||||
app.include_router(search.router, prefix="/api/search", tags=["Search"])
|
||||
app.include_router(index.router, prefix=f"{PREFIX}/index", tags=["Index"])
|
||||
app.include_router(bookmarks.router, prefix=f"{PREFIX}/bookmarks", tags=["Bookmarks"])
|
||||
app.include_router(favorites.router, prefix=f"{PREFIX}/favorites", tags=["Favorites"])
|
||||
app.include_router(search.router, prefix=f"{PREFIX}/search", tags=["Search"])
|
||||
|
||||
app.include_router(proxy.router, prefix="/api/proxy")
|
||||
app.include_router(proxy.router, prefix=f"{PREFIX}/proxy")
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8000)
|
||||
|
|
|
@ -27,7 +27,9 @@ services:
|
|||
dockerfile: ../docker/backend.dev.Dockerfile
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.anix-backend.rule=Host(`127.0.0.1`) && PathPrefix(`/api`)"
|
||||
- "traefik.http.routers.anix-backend.rule=Host(`127.0.0.1`) && PathPrefix(`/api/`)"
|
||||
- traefik.http.middlewares.anix-backend_stripprefix.stripprefix.prefixes=/api
|
||||
- "traefik.http.routers.anix-backend.middlewares=anix-backend_stripprefix@docker"
|
||||
- "traefik.http.routers.anix-backend.entrypoints=web"
|
||||
expose:
|
||||
- 8000
|
||||
|
@ -43,12 +45,13 @@ services:
|
|||
container_name: "AniX-traefik"
|
||||
command:
|
||||
#- "--log.level=DEBUG"
|
||||
- "--api.dashboard=false"
|
||||
- "--api.insecure=false"
|
||||
- "--api.dashboard=true"
|
||||
- "--api.insecure=true"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entryPoints.web.address=:80"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
|
|
|
@ -15,4 +15,4 @@ COPY . .
|
|||
# Install any needed packages specified in requirements.txt
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0"]
|
||||
CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--root-path", "/api"]
|
||||
|
|
|
@ -56,8 +56,8 @@ export const App = (props) => {
|
|||
|
||||
return (
|
||||
<body>
|
||||
<div style={{ display: "flex", "flex-direction": "row" }}>
|
||||
<div style={{ "padding-inline-start": "0" }}>
|
||||
<div style={{ display: "flex", flexDirection: "row" }}>
|
||||
<div style={{ paddingInlineStart: "0" }}>
|
||||
<NavigationRail
|
||||
colorPicker={colorPicker}
|
||||
settingsPopup={settingsPopup}
|
||||
|
@ -89,7 +89,7 @@ export const App = (props) => {
|
|||
>
|
||||
<div
|
||||
className="border round padding"
|
||||
style={{ height: "calc(100vh - 2rem)", "overflow-y": "scroll" }}
|
||||
style={{ height: "calc(100vh - 2rem)", overflowY: "scroll" }}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export const API_URL = "/api";
|
||||
export let API_URL = "/api/v1";
|
||||
|
||||
export const endpoints = {
|
||||
index: {
|
||||
|
|
|
@ -1,122 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { getData } from "@/app/api/api-utils";
|
||||
import { endpoints } from "@/app/api/config";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import ReleasesOverview from "@/app/components/ReleasesOverview/ReleasesOverview";
|
||||
import { useUserStore } from "@/app/store/user-store";
|
||||
import { LogInNeeded } from "@/app/components/LogInNeeded/LogInNeeded";
|
||||
import BookmarksPage from "../components/Pages/BookmarksPage";
|
||||
|
||||
export default function Bookmarks() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const [list, setList] = useState();
|
||||
const [releases, setReleases] = useState();
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
const [isNextPage, setIsNextPage] = useState(true);
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const createQueryString = useCallback(
|
||||
(name, value) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(name, value);
|
||||
|
||||
return params.toString();
|
||||
},
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
// set list on initial page load
|
||||
useEffect(() => {
|
||||
const query = searchParams.get("list");
|
||||
if (query) {
|
||||
setList(query);
|
||||
} else {
|
||||
setList("watching");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
async function fetchData(list, page = 0) {
|
||||
if (userStore.token) {
|
||||
const url = `${endpoints.user.bookmarks[list]}?page=${page}&token=${userStore.token}`;
|
||||
const data = await getData(url);
|
||||
|
||||
if (data.content.length < 25) {
|
||||
setIsNextPage(false);
|
||||
} else {
|
||||
setIsNextPage(true);
|
||||
}
|
||||
|
||||
// Handle initial load (page 0) or subsequent pagination
|
||||
if (page === 0) {
|
||||
setReleases(data.content);
|
||||
} else {
|
||||
setReleases([...releases, ...data.content]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (list) {
|
||||
router.push(pathname + "?" + createQueryString("list", list));
|
||||
setReleases(null);
|
||||
setPage(0);
|
||||
fetchData(list); // Call fetchData here
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [list, userStore.token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (list && releases) {
|
||||
fetchData(list, page); // Use fetchData for pagination
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page]);
|
||||
|
||||
const chips = [
|
||||
{
|
||||
title: "Смотрю",
|
||||
list: "watching",
|
||||
},
|
||||
{
|
||||
title: "В планах",
|
||||
list: "planned",
|
||||
},
|
||||
{
|
||||
title: "Просмотрено",
|
||||
list: "watched",
|
||||
},
|
||||
{
|
||||
title: "Отложено",
|
||||
list: "delayed",
|
||||
},
|
||||
{
|
||||
title: "Заброшено",
|
||||
list: "abandoned",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{!userStore.isAuth ? (
|
||||
<LogInNeeded />
|
||||
) : (
|
||||
<ReleasesOverview
|
||||
chips={chips}
|
||||
setList={setList}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
list={list}
|
||||
releases={releases}
|
||||
isNextPage={isNextPage}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return <>{!userStore.isAuth ? <LogInNeeded /> : <BookmarksPage />}</>;
|
||||
}
|
||||
|
|
|
@ -47,13 +47,13 @@ export const NavigationRail = (props) => {
|
|||
<nav
|
||||
className="left border round margin"
|
||||
style={{
|
||||
"inline-size": "unset",
|
||||
inlineSize: "unset",
|
||||
position: "sticky",
|
||||
top: "1rem",
|
||||
left: "0",
|
||||
"min-height": "calc(100vh - (var(---margin) * 2))",
|
||||
"background-color": "var(--surface)",
|
||||
"padding-block": "1rem",
|
||||
minHeight: "calc(100vh - (var(---margin) * 2))",
|
||||
backgroundColor: "var(--surface)",
|
||||
paddingBlock: "1rem",
|
||||
}}
|
||||
>
|
||||
{userStore.isAuth && userStore.user ? (
|
||||
|
|
117
frontend/app/components/Pages/BookmarksPage.jsx
Normal file
117
frontend/app/components/Pages/BookmarksPage.jsx
Normal file
|
@ -0,0 +1,117 @@
|
|||
"use client";
|
||||
|
||||
import { getData } from "@/app/api/api-utils";
|
||||
import { endpoints } from "@/app/api/config";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import ReleasesOverview from "@/app/components/ReleasesOverview/ReleasesOverview";
|
||||
import { useUserStore } from "@/app/store/user-store";
|
||||
|
||||
export default function BookmarksPage() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const [list, setList] = useState();
|
||||
const [releases, setReleases] = useState();
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
const [isNextPage, setIsNextPage] = useState(true);
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const createQueryString = useCallback(
|
||||
(name, value) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(name, value);
|
||||
|
||||
return params.toString();
|
||||
},
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
// set list on initial page load
|
||||
useEffect(() => {
|
||||
const query = searchParams.get("list");
|
||||
if (query) {
|
||||
setList(query);
|
||||
} else {
|
||||
setList("watching");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
async function fetchData(list, page = 0) {
|
||||
if (userStore.token) {
|
||||
const url = `${endpoints.user.bookmarks[list]}?page=${page}&token=${userStore.token}`;
|
||||
const data = await getData(url);
|
||||
|
||||
if (data.content.length < 25) {
|
||||
setIsNextPage(false);
|
||||
} else {
|
||||
setIsNextPage(true);
|
||||
}
|
||||
|
||||
// Handle initial load (page 0) or subsequent pagination
|
||||
if (page === 0) {
|
||||
setReleases(data.content);
|
||||
} else {
|
||||
setReleases([...releases, ...data.content]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (list) {
|
||||
router.push(pathname + "?" + createQueryString("list", list));
|
||||
setReleases(null);
|
||||
setPage(0);
|
||||
fetchData(list); // Call fetchData here
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [list, userStore.token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (list && releases) {
|
||||
fetchData(list, page); // Use fetchData for pagination
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page]);
|
||||
|
||||
const chips = [
|
||||
{
|
||||
title: "Смотрю",
|
||||
list: "watching",
|
||||
},
|
||||
{
|
||||
title: "В планах",
|
||||
list: "planned",
|
||||
},
|
||||
{
|
||||
title: "Просмотрено",
|
||||
list: "watched",
|
||||
},
|
||||
{
|
||||
title: "Отложено",
|
||||
list: "delayed",
|
||||
},
|
||||
{
|
||||
title: "Заброшено",
|
||||
list: "abandoned",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReleasesOverview
|
||||
chips={chips}
|
||||
setList={setList}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
list={list}
|
||||
releases={releases}
|
||||
isNextPage={isNextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
107
frontend/app/components/Pages/IndexPage.jsx
Normal file
107
frontend/app/components/Pages/IndexPage.jsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
"use client";
|
||||
|
||||
import { getData } from "@/app/api/api-utils";
|
||||
import { endpoints } from "@/app/api/config";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import ReleasesOverview from "@/app/components/ReleasesOverview/ReleasesOverview";
|
||||
|
||||
export default function IndexPage() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const [list, setList] = useState();
|
||||
const [releases, setReleases] = useState();
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
const [isNextPage, setIsNextPage] = useState(true);
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const createQueryString = useCallback(
|
||||
(name, value) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(name, value);
|
||||
|
||||
return params.toString();
|
||||
},
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
// set list on initial page load
|
||||
useEffect(() => {
|
||||
const query = searchParams.get("list");
|
||||
if (query) {
|
||||
setList(query);
|
||||
} else {
|
||||
setList("last");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
async function fetchData(list, page = 0) {
|
||||
const url = `${endpoints.index[list]}?page=${page}`;
|
||||
const data = await getData(url);
|
||||
|
||||
if (data.content.length < 25) {
|
||||
setIsNextPage(false);
|
||||
} else {
|
||||
setIsNextPage(true);
|
||||
}
|
||||
|
||||
// Handle initial load (page 0) or subsequent pagination
|
||||
if (page === 0) {
|
||||
setReleases(data.content);
|
||||
} else {
|
||||
setReleases([...releases, ...data.content]);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (list) {
|
||||
router.push(pathname + "?" + createQueryString("list", list));
|
||||
setReleases(null);
|
||||
setPage(0);
|
||||
fetchData(list); // Call fetchData here
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [list]);
|
||||
|
||||
useEffect(() => {
|
||||
if (list && releases) {
|
||||
fetchData(list, page); // Use fetchData for pagination
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page]);
|
||||
|
||||
const chips = [
|
||||
{
|
||||
title: "последнее",
|
||||
list: "last",
|
||||
},
|
||||
{
|
||||
title: "в эфире",
|
||||
list: "ongoing",
|
||||
},
|
||||
{
|
||||
title: "анонсировано",
|
||||
list: "announce",
|
||||
},
|
||||
{
|
||||
title: "завершено",
|
||||
list: "finished",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ReleasesOverview
|
||||
chips={chips}
|
||||
setList={setList}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
list={list}
|
||||
releases={releases}
|
||||
isNextPage={isNextPage}
|
||||
/>
|
||||
);
|
||||
}
|
150
frontend/app/components/Pages/SearchPage.jsx
Normal file
150
frontend/app/components/Pages/SearchPage.jsx
Normal file
|
@ -0,0 +1,150 @@
|
|||
"use client";
|
||||
|
||||
import { getData } from "@/app/api/api-utils";
|
||||
import { endpoints } from "@/app/api/config";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import ReleasesOverview from "@/app/components/ReleasesOverview/ReleasesOverview";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
function saveSearches(search) {
|
||||
localStorage.setItem("searches", search);
|
||||
}
|
||||
function getSearches() {
|
||||
return localStorage.getItem("searches");
|
||||
}
|
||||
|
||||
export default function SearchPage() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const [releases, setReleases] = useState();
|
||||
const [page, setPage] = useState(0);
|
||||
const [query, setQuery] = useState("");
|
||||
const [isNextPage, setIsNextPage] = useState(true);
|
||||
|
||||
const [searches, setSearches] = useState(JSON.parse(getSearches()));
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const createQueryString = useCallback(
|
||||
(name, value) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(name, value);
|
||||
|
||||
return params.toString();
|
||||
},
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
async function fetchData(query, page = 0) {
|
||||
const url = `${endpoints.search}?query=${query}&page=${page}`;
|
||||
const data = await getData(url);
|
||||
|
||||
if (data.content.length < 25) {
|
||||
setIsNextPage(false);
|
||||
} else {
|
||||
setIsNextPage(true);
|
||||
}
|
||||
|
||||
// Handle initial load (page 0) or subsequent pagination
|
||||
if (page === 0) {
|
||||
setReleases(data.content);
|
||||
} else {
|
||||
setReleases([...releases, ...data.content]);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const query = searchParams.get("query");
|
||||
if (query) {
|
||||
setQuery(query);
|
||||
fetchData(query, 0);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (releases) {
|
||||
fetchData(query, page); // Use fetchData for pagination
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page]);
|
||||
|
||||
const handleInput = (e) => {
|
||||
setQuery(e.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (query != "") {
|
||||
router.push(pathname + "?" + createQueryString("query", query));
|
||||
setReleases(null);
|
||||
setPage(0);
|
||||
fetchData(query);
|
||||
|
||||
// save searches and update search history
|
||||
if (!searches) {
|
||||
setSearches([query]);
|
||||
saveSearches(JSON.stringify([query]));
|
||||
} else {
|
||||
console.log(searches);
|
||||
if (!searches.find((element) => element == query)) {
|
||||
setSearches([query, ...searches.slice(0, 5)]);
|
||||
saveSearches(JSON.stringify([query, ...searches.slice(0, 5)]));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<form className="field large prefix round fill" onSubmit={handleSubmit}>
|
||||
<i className="front">search</i>
|
||||
<input name="query" onInput={handleInput} value={query} />
|
||||
<menu className="min" style={{ marginTop: "64px" }}>
|
||||
{searches
|
||||
? searches.map((item) => {
|
||||
return (
|
||||
<a
|
||||
key={item}
|
||||
onClick={() => {
|
||||
setQuery(item);
|
||||
}}
|
||||
className="row"
|
||||
>
|
||||
<i>history</i>
|
||||
<div>{item}</div>
|
||||
</a>
|
||||
);
|
||||
})
|
||||
: ""}
|
||||
</menu>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{releases ? (
|
||||
releases.length > 0 ? (
|
||||
<ReleasesOverview
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
releases={releases}
|
||||
isNextPage={isNextPage}
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute padding primary center middle small-round">
|
||||
<i className="extra">search</i>
|
||||
<h5>Ничего не найдено.</h5>
|
||||
<p>Введите другой поисковой запрос.</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="absolute padding primary center middle small-round">
|
||||
<i className="extra">search</i>
|
||||
<h5>Здесь пока ничего нет.</h5>
|
||||
<p>Введите поисковой запрос для начала поиска.</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,107 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import { getData } from "./api/api-utils";
|
||||
import { endpoints } from "./api/config";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import ReleasesOverview from "./components/ReleasesOverview/ReleasesOverview";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const IndexPage = dynamic(() => import("./components/Pages/IndexPage"), {
|
||||
ssr: false,
|
||||
});
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const [list, setList] = useState();
|
||||
const [releases, setReleases] = useState();
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
const [isNextPage, setIsNextPage] = useState(true);
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const createQueryString = useCallback(
|
||||
(name, value) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(name, value);
|
||||
|
||||
return params.toString();
|
||||
},
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
// set list on initial page load
|
||||
useEffect(() => {
|
||||
const query = searchParams.get("list");
|
||||
if (query) {
|
||||
setList(query);
|
||||
} else {
|
||||
setList("last");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
async function fetchData(list, page = 0) {
|
||||
const url = `${endpoints.index[list]}?page=${page}`;
|
||||
const data = await getData(url);
|
||||
|
||||
if (data.content.length < 25) {
|
||||
setIsNextPage(false);
|
||||
} else {
|
||||
setIsNextPage(true);
|
||||
}
|
||||
|
||||
// Handle initial load (page 0) or subsequent pagination
|
||||
if (page === 0) {
|
||||
setReleases(data.content);
|
||||
} else {
|
||||
setReleases([...releases, ...data.content]);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (list) {
|
||||
router.push(pathname + "?" + createQueryString("list", list));
|
||||
setReleases(null);
|
||||
setPage(0);
|
||||
fetchData(list); // Call fetchData here
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [list]);
|
||||
|
||||
useEffect(() => {
|
||||
if (list && releases) {
|
||||
fetchData(list, page); // Use fetchData for pagination
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page]);
|
||||
|
||||
const chips = [
|
||||
{
|
||||
title: "последнее",
|
||||
list: "last",
|
||||
},
|
||||
{
|
||||
title: "в эфире",
|
||||
list: "ongoing",
|
||||
},
|
||||
{
|
||||
title: "анонсировано",
|
||||
list: "announce",
|
||||
},
|
||||
{
|
||||
title: "завершено",
|
||||
list: "finished",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ReleasesOverview
|
||||
chips={chips}
|
||||
setList={setList}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
list={list}
|
||||
releases={releases}
|
||||
isNextPage={isNextPage}
|
||||
/>
|
||||
<>
|
||||
<IndexPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,150 +1,13 @@
|
|||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { getData } from "@/app/api/api-utils";
|
||||
import { endpoints } from "@/app/api/config";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import ReleasesOverview from "../components/ReleasesOverview/ReleasesOverview";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
function saveSearches(search) {
|
||||
localStorage.setItem("searches", search);
|
||||
}
|
||||
function getSearches() {
|
||||
return localStorage.getItem("searches");
|
||||
}
|
||||
const SearchPage = dynamic(() => import("../components/Pages/SearchPage"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default function Search() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const [releases, setReleases] = useState();
|
||||
const [page, setPage] = useState(0);
|
||||
const [query, setQuery] = useState("");
|
||||
const [isNextPage, setIsNextPage] = useState(true);
|
||||
|
||||
const [searches, setSearches] = useState(JSON.parse(getSearches()));
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const createQueryString = useCallback(
|
||||
(name, value) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(name, value);
|
||||
|
||||
return params.toString();
|
||||
},
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
async function fetchData(query, page = 0) {
|
||||
const url = `${endpoints.search}?query=${query}&page=${page}`;
|
||||
const data = await getData(url);
|
||||
|
||||
if (data.content.length < 25) {
|
||||
setIsNextPage(false);
|
||||
} else {
|
||||
setIsNextPage(true);
|
||||
}
|
||||
|
||||
// Handle initial load (page 0) or subsequent pagination
|
||||
if (page === 0) {
|
||||
setReleases(data.content);
|
||||
} else {
|
||||
setReleases([...releases, ...data.content]);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const query = searchParams.get("query");
|
||||
if (query) {
|
||||
setQuery(query);
|
||||
fetchData(query, 0);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (releases) {
|
||||
fetchData(query, page); // Use fetchData for pagination
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page]);
|
||||
|
||||
const handleInput = (e) => {
|
||||
setQuery(e.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (query != "") {
|
||||
router.push(pathname + "?" + createQueryString("query", query));
|
||||
setReleases(null);
|
||||
setPage(0);
|
||||
fetchData(query);
|
||||
|
||||
// save searches and update search history
|
||||
if (!searches) {
|
||||
setSearches([query]);
|
||||
saveSearches(JSON.stringify([query]));
|
||||
} else {
|
||||
console.log(searches);
|
||||
if (!searches.find((element) => element == query)) {
|
||||
setSearches([query, ...searches.slice(0, 5)]);
|
||||
saveSearches(JSON.stringify([query, ...searches.slice(0, 5)]));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<form className="field large prefix round fill" onSubmit={handleSubmit}>
|
||||
<i className="front">search</i>
|
||||
<input name="query" onInput={handleInput} value={query} />
|
||||
<menu className="min" style={{ marginTop: "64px" }}>
|
||||
{searches
|
||||
? searches.map((item) => {
|
||||
return (
|
||||
<a
|
||||
key={item}
|
||||
onClick={() => {
|
||||
setQuery(item);
|
||||
}}
|
||||
className="row"
|
||||
>
|
||||
<i>history</i>
|
||||
<div>{item}</div>
|
||||
</a>
|
||||
);
|
||||
})
|
||||
: ""}
|
||||
</menu>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{releases ? (
|
||||
releases.length > 0 ? (
|
||||
<ReleasesOverview
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
releases={releases}
|
||||
isNextPage={isNextPage}
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute padding primary center middle small-round">
|
||||
<i className="extra">search</i>
|
||||
<h5>Ничего не найдено.</h5>
|
||||
<p>Введите другой поисковой запрос.</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="absolute padding primary center middle small-round">
|
||||
<i className="extra">search</i>
|
||||
<h5>Здесь пока ничего нет.</h5>
|
||||
<p>Введите поисковой запрос для начала поиска.</p>
|
||||
</div>
|
||||
)}
|
||||
<SearchPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
reactStrictMode: false,
|
||||
images: {
|
||||
remotePatterns: [
|
||||
|
|
38
frontend/package-lock.json
generated
38
frontend/package-lock.json
generated
|
@ -13,13 +13,13 @@
|
|||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"beercss": "^3.5.1",
|
||||
"material-dynamic-colors": "^1.1.0",
|
||||
"next": "^14.2.2",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-config-next": "14.2.2",
|
||||
"next": "^14.2.2",
|
||||
"postcss": "^8"
|
||||
}
|
||||
},
|
||||
|
@ -228,8 +228,7 @@
|
|||
"node_modules/@next/env": {
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz",
|
||||
"integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA=="
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
"version": "14.2.2",
|
||||
|
@ -247,7 +246,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
|
@ -263,7 +261,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
|
@ -279,7 +276,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
|
@ -295,7 +291,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
|
@ -311,7 +306,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
|
@ -327,7 +321,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
|
@ -343,7 +336,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
|
@ -359,7 +351,6 @@
|
|||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
|
@ -375,7 +366,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
|
@ -438,14 +428,12 @@
|
|||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
|
||||
"integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3",
|
||||
"tslib": "^2.4.0"
|
||||
|
@ -918,7 +906,6 @@
|
|||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"streamsearch": "^1.1.0"
|
||||
},
|
||||
|
@ -959,7 +946,6 @@
|
|||
"version": "1.0.30001612",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz",
|
||||
"integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
@ -995,8 +981,7 @@
|
|||
"node_modules/client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
|
@ -2161,8 +2146,7 @@
|
|||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
|
||||
},
|
||||
"node_modules/graphemer": {
|
||||
"version": "1.4.0",
|
||||
|
@ -2944,7 +2928,6 @@
|
|||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
@ -2969,7 +2952,6 @@
|
|||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz",
|
||||
"integrity": "sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@next/env": "14.2.3",
|
||||
"@swc/helpers": "0.5.5",
|
||||
|
@ -3019,7 +3001,6 @@
|
|||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
@ -3303,8 +3284,7 @@
|
|||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
|
@ -3750,7 +3730,6 @@
|
|||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
@ -3759,7 +3738,6 @@
|
|||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
|
@ -3955,7 +3933,6 @@
|
|||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
|
||||
"integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"client-only": "0.0.1"
|
||||
},
|
||||
|
@ -4054,8 +4031,7 @@
|
|||
"node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
|
|
Loading…
Add table
Reference in a new issue