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:
Kentai Radiquum 2024-05-13 22:20:08 +05:00
parent d85ce45989
commit d97ad7dbfe
Signed by: Radiquum
GPG key ID: 858E8EE696525EED
19 changed files with 504 additions and 412 deletions

4
.gitignore vendored
View file

@ -7,8 +7,12 @@ venv
.VSCode
*.code-workspace
# DetaSpace
.space
# NextJS
## dependencies
standalone
node_modules
.pnp
.pnp.js

57
.spaceignore Normal file
View 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

View file

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

View file

@ -1,5 +1,7 @@
# TODO
- [ ] Add docker deployment
## Релизы
- [ ] Авто-ген ссылка с именем

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
export const API_URL = "/api";
export let API_URL = "/api/v1";
export const endpoints = {
index: {

View file

@ -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 />}</>;
}

View file

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

View 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}
/>
</>
);
}

View 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}
/>
);
}

View 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>
)}
</>
);
}

View file

@ -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 />
</>
);
}

View file

@ -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 />
</>
);
}

View file

@ -1,5 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
reactStrictMode: false,
images: {
remotePatterns: [

View file

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