diff --git a/components/alert.js b/components/alert.js new file mode 100644 index 0000000..372e65a --- /dev/null +++ b/components/alert.js @@ -0,0 +1,129 @@ +import { useState, useEffect, useRef } from "react"; +import { useRouter } from "next/router"; +import PropTypes from "prop-types"; + +import { alertService, AlertType } from "../services"; + +export { Alert }; + +Alert.propTypes = { + id: PropTypes.string, + fade: PropTypes.bool, +}; + +Alert.defaultProps = { + id: "default-alert", + fade: true, +}; + +function Alert({ id, fade }) { + const mounted = useRef(false); + const router = useRouter(); + const [alerts, setAlerts] = useState([]); + + useEffect(() => { + mounted.current = true; + + // subscribe to new alert notifications + const subscription = alertService.onAlert(id).subscribe((alert) => { + // clear alerts when an empty alert is received + if (!alert.message) { + setAlerts((alerts) => { + // filter out alerts without 'keepAfterRouteChange' flag + const filteredAlerts = alerts.filter((x) => x.keepAfterRouteChange); + + // remove 'keepAfterRouteChange' flag on the rest + return omit(filteredAlerts, "keepAfterRouteChange"); + }); + } else { + // add alert to array with unique id + alert.itemId = Math.random(); + setAlerts((alerts) => [...alerts, alert]); + + // auto close alert if required + if (alert.autoClose) { + setTimeout(() => removeAlert(alert), 3000); + } + } + }); + + // clear alerts on location change + const clearAlerts = () => alertService.clear(id); + router.events.on("routeChangeStart", clearAlerts); + + // clean up function that runs when the component unmounts + return () => { + mounted.current = false; + + // unsubscribe to avoid memory leaks + subscription.unsubscribe(); + router.events.off("routeChangeStart", clearAlerts); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + function omit(arr, key) { + return arr.map((obj) => { + const { [key]: omitted, ...rest } = obj; + return rest; + }); + } + + function removeAlert(alert) { + if (!mounted.current) return; + + if (fade) { + // fade out alert + setAlerts((alerts) => + alerts.map((x) => + x.itemId === alert.itemId ? { ...x, fade: true } : x + ) + ); + + // remove alert after faded out + setTimeout(() => { + setAlerts((alerts) => alerts.filter((x) => x.itemId !== alert.itemId)); + }, 250); + } else { + // remove alert + setAlerts((alerts) => alerts.filter((x) => x.itemId !== alert.itemId)); + } + } + + function cssClasses(alert) { + if (!alert) return; + + const classes = ["alert", "alert-dismissable"]; + + const alertTypeClass = { + [AlertType.Success]: "alert-success", + [AlertType.Error]: "alert-danger", + [AlertType.Info]: "alert-info", + [AlertType.Warning]: "alert-warning", + }; + + classes.push(alertTypeClass[alert.type]); + + if (alert.fade) { + classes.push("fade"); + } + + return classes.join(" "); + } + + if (!alerts.length) return null; + + return ( +
+ {alerts.map((alert, index) => ( +
+ removeAlert(alert)}> + × + + {alert.message} +
+ ))} +
+ ); +} diff --git a/components/borderImage.js b/components/borderImage.js new file mode 100644 index 0000000..8578a2d --- /dev/null +++ b/components/borderImage.js @@ -0,0 +1,37 @@ +import Image from "next/image"; +import styles from "../styles/Components.module.css"; + +export default function BorderImage(props) { + const { key, border, selected, size, onSelect, customImage } = props; + + const imageSize = size || 128; + + const classNames = [ + styles.borderKeeper, + selected == border.id ? styles.selected : undefined, + ].join(" "); + return ( +
+ user image { + onSelect && onSelect(border.id); + }} + /> + {`border { + onSelect && onSelect(border.id); + }} + /> +
+ ); +} diff --git a/components/borderPreview.js b/components/borderPreview.js new file mode 100644 index 0000000..5bac6e8 --- /dev/null +++ b/components/borderPreview.js @@ -0,0 +1,46 @@ +import BorderImage from "./borderImage"; +import styles from "../styles/Components.module.css"; +import { useSession } from "next-auth/react"; + +export default function Preview(props) { + const { data: session } = useSession(); + const { data, current, selected, apply } = props; + + const currentItem = data?.filter( + (item) => parseInt(item.id) === (current != null ? parseInt(current) : 0) + )?.[0]; + + const selectedItem = data?.filter( + (item) => parseInt(item.id) === parseInt(selected) + )?.[0]; + + console.log(currentItem, selectedItem, session); + + return ( +
+ current +
+ {currentItem && ( + + )} +
+ new +
+ {selectedItem && ( + + )} +
+ +
+ ); +} diff --git a/components/borderSelect.js b/components/borderSelect.js deleted file mode 100644 index dfdacde..0000000 --- a/components/borderSelect.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function BorderSelect() { - -} \ No newline at end of file diff --git a/components/select.js b/components/select.js index 5c94a37..f0170ed 100644 --- a/components/select.js +++ b/components/select.js @@ -1,43 +1,47 @@ -import Image from "next/image"; +import BorderImage from "./borderImage"; +import InfiniteScroll from "react-infinite-scroll-component"; +import { useEffect, useState } from "react"; import styles from "../styles/Components.module.css"; +const pageSize = 48; + export default function Select(props) { - const { data, onSelect, selected = {} } = props; - console.log("data", data, "selected", selected); + const { data, total, onSelect, selected = {} } = props; + console.log("data", data, "selected", selected, total); + + const [scrollIndex, setScrollIndex] = useState(36); + const [scrollData, setScrollData] = useState([]); + + useEffect(() => { + setScrollData(data.slice(0, pageSize - 1)); + }, [data]); + + const fetchMore = () => { + let newData = data.slice(scrollIndex, scrollIndex + pageSize); + setScrollData([...scrollData, ...newData]); + setScrollIndex(scrollIndex + pageSize); + }; + return ( -
- {data && - data.map((border, index) => { + scrollData?.length || 0} + height={"75vh"} + className={styles.select} + loader={

Loading...

} + endMessage={

No more borders.

}> + {scrollData && + scrollData.map((border, index) => { return ( -
- user image { - onSelect(border.id); - }} - /> - {`border { - onSelect(border.id); - }} - /> -
+ ); })} -
+ ); } diff --git a/lib/borders.js b/lib/borders.js index bd65106..8d7c41e 100644 --- a/lib/borders.js +++ b/lib/borders.js @@ -9,10 +9,21 @@ export const getBorderById = async (id) => { }); }; +export const countAllBorders = async () => { + return await prisma.borderImage.count(); +}; + export const getAllBorders = async (limit = undefined, cursor = undefined) => { + const sanitizedLimit = parseInt(limit) || undefined; + const sanitizedCursor = parseInt(cursor) || 0; return await prisma.borderImage.findMany({ - take: limit, - cursor: cursor, + take: sanitizedLimit, + cursor: { + id: sanitizedCursor, + }, + orderBy: { + id: "asc", + }, }); }; @@ -63,10 +74,10 @@ export const setUserBorder = async (req, borderId) => { create: { userId: session.user.id, discordId: accountData.providerAccountId, - borderId, + borderId: parseInt(borderId), }, - data: { - borderId, + update: { + borderId: parseInt(borderId), }, where: { userId: session.user.id, diff --git a/next.config.js b/next.config.js index 45c7eac..96bc66a 100644 --- a/next.config.js +++ b/next.config.js @@ -8,4 +8,7 @@ module.exports = { config.plugins.push(new webpack.EnvironmentPlugin(environment)); return config; }, + images: { + domains: ["localhost", "borders.j4.pm", "cdn.discordapp.com"], + }, }; diff --git a/package.json b/package.json index 1b30b22..2bb38d8 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "node-fetch": "^3.2.3", "prisma": "^3.12.0", "react": "18.0.0", - "react-dom": "18.0.0" + "react-dom": "18.0.0", + "react-infinite-scroll-component": "^6.1.0", + "rxjs": "^7.5.5" }, "devDependencies": { "eslint": "8.13.0", diff --git a/pages/_app.js b/pages/_app.js index 162643c..90fc6b9 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -1,9 +1,11 @@ import "../styles/globals.css"; import { SessionProvider } from "next-auth/react"; +import { Alert } from "../components/alert"; function MyApp({ Component, pageProps: { session, ...pageProps } }) { return ( + ); diff --git a/pages/api/auth/[...nextauth].js b/pages/api/auth/[...nextauth].js index d8016d6..8d37a71 100644 --- a/pages/api/auth/[...nextauth].js +++ b/pages/api/auth/[...nextauth].js @@ -12,6 +12,20 @@ export default NextAuth({ }), ], callbacks: { + async signIn({ user, account, profile, email, credentials }) { + console.log(user, account, profile, email, credentials); + if (user.image != profile.image_url) { + await prisma.user.update({ + data: { + image: profile.image_url, + }, + where: { + id: user.id, + }, + }); + } + return true; + }, async session({ session, token, user }) { session.user.id = user.id; // console.log(JSON.stringify(user)); diff --git a/pages/api/border/all.js b/pages/api/border/all.js index b189ccf..c450d24 100644 --- a/pages/api/border/all.js +++ b/pages/api/border/all.js @@ -1,7 +1,9 @@ -import { getAllBorders } from "../../../lib/borders"; +import { getAllBorders, countAllBorders } from "../../../lib/borders"; export default function handler(req, res) { - getAllBorders().then((result) => { - return res.status(200).json(result); + getAllBorders(req.query?.limit, req.query?.cursor).then((result) => { + countAllBorders().then((count) => { + return res.status(200).json({ data: result, count }); + }); }); } diff --git a/pages/api/hello.js b/pages/api/hello.js index df63de8..71543a5 100644 --- a/pages/api/hello.js +++ b/pages/api/hello.js @@ -1,5 +1,7 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction export default function handler(req, res) { - res.status(200).json({ name: 'John Doe' }) + res.status(200).json({ + ok: "ok", + }); } diff --git a/pages/api/user/border/@me.js b/pages/api/user/border/@me.js index 61cf0e8..bbebac6 100644 --- a/pages/api/user/border/@me.js +++ b/pages/api/user/border/@me.js @@ -1,11 +1,21 @@ -import { getUserBorders } from "../../../../lib/borders"; +import { getUserBorders, setUserBorder } from "../../../../lib/borders"; export default function handler(req, res) { - getUserBorders(req).then((result) => { - if (result) { - return res.status(200).json(result); - } else { - return res.status(404).json({ error: "Not Found" }); - } - }); + if (req.method === "POST") { + setUserBorder(req, req.body).then((result) => { + if (result) { + return res.status(200).json(result); + } else { + return res.status(500).json({ error: "could not update border" }); + } + }); + } else { + getUserBorders(req).then((result) => { + if (result) { + return res.status(200).json(result); + } else { + return res.status(404).json({ error: "Not Found" }); + } + }); + } } diff --git a/pages/index.js b/pages/index.js index 065805e..73624a0 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,35 +1,91 @@ import Head from "next/head"; import Image from "next/image"; +import Router from "next/router"; import styles from "../styles/Home.module.css"; import UserInfo from "../components/userInfo"; +import Preview from "../components/borderPreview"; import Select from "../components/select"; +import { alertService } from "../services"; import { useEffect, useState } from "react"; export default function Home() { - const [data, setData] = useState(null); + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); const [borderData, setBorderData] = useState(null); const [selected, setSelected] = useState(0); - useEffect(() => { - fetch("api/border/all") + // const pageSize = 36; + + const [cursor, setCursor] = useState(0); + + // const fetchMore = () => { + // console.log("fetch more"); + // fetch(`api/border/all?limit=${pageSize}&cursor=${cursor}`) + // .then((res) => res.json()) + // .then((res) => { + // setData([...data, ...res.data]); + // setTotal(res.count); + // }); + // }; + + const applyBorder = () => { + console.log("apply"); + fetch("api/user/border/@me", { method: "POST", body: selected }) .then((res) => res.json()) - .then((data) => setData(data)); - }, []); + .then((res) => { + console.log(res); + if (res.error) { + alertService.error(`error: ${res.error}`); + } else { + setBorderData(res); + } + }); + }; useEffect(() => { fetch("api/user/border/@me") + .then((res) => res.json()) + .then((dat) => { + console.log("border data", dat); + setBorderData(dat); + setSelected(dat?.borderId || 0); + }); + }, [data]); + + useEffect(() => { + fetch(`api/border/all`) .then((res) => res.json()) .then((data) => { - setBorderData(data); - setSelected(data?.borderId || 0); + setData(data.data); + setTotal(data.count); }); }, []); + // useEffect(() => { + // let final = data?.[data?.length - 1]; + // if (final) { + // setCursor(parseInt(final.id) + 1); + // console.log(cursor); + // } + // }, [data, cursor]); + + useEffect(() => { + const timer = setTimeout(() => { + // if data has not loaded in 5 seconds + if (!data || !borderData) { + console.log(data, borderData); + Router.reload(); + } else { + console.log("data loaded properly, not reloading."); + } + }, 5000); + return () => clearTimeout(timer); + }, [data, borderData]); + return (
- Create Next App - + Borders
@@ -39,7 +95,18 @@ export default function Home() {
-
); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be0e922..d683285 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,8 @@ specifiers: prisma: ^3.12.0 react: 18.0.0 react-dom: 18.0.0 + react-infinite-scroll-component: ^6.1.0 + rxjs: ^7.5.5 dependencies: '@next-auth/prisma-adapter': 1.0.3_2b535bbfb604d219371c9582b52b11b1 @@ -23,6 +25,8 @@ dependencies: prisma: 3.12.0 react: 18.0.0 react-dom: 18.0.0_react@18.0.0 + react-infinite-scroll-component: 6.1.0_react@18.0.0 + rxjs: 7.5.5 devDependencies: eslint: 8.13.0 @@ -1610,6 +1614,15 @@ packages: scheduler: 0.21.0 dev: false + /react-infinite-scroll-component/6.1.0_react@18.0.0: + resolution: {integrity: sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==} + peerDependencies: + react: '>=16.0.0' + dependencies: + react: 18.0.0 + throttle-debounce: 2.3.0 + dev: false + /react-is/16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: true @@ -1676,6 +1689,12 @@ packages: queue-microtask: 1.2.3 dev: true + /rxjs/7.5.5: + resolution: {integrity: sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==} + dependencies: + tslib: 2.4.0 + dev: false + /scheduler/0.21.0: resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==} dependencies: @@ -1801,6 +1820,11 @@ packages: resolution: {integrity: sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=} dev: true + /throttle-debounce/2.3.0: + resolution: {integrity: sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==} + engines: {node: '>=8'} + dev: false + /to-regex-range/5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1821,6 +1845,10 @@ packages: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: true + /tslib/2.4.0: + resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} + dev: false + /tsutils/3.21.0: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5b7f6f8..05c4b44 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -71,4 +71,5 @@ model BorderImage { appId Int? appName String? borderName String? + itemType Int? } diff --git a/services/alert.service.js b/services/alert.service.js new file mode 100644 index 0000000..e66f5c0 --- /dev/null +++ b/services/alert.service.js @@ -0,0 +1,56 @@ +import { Subject } from "rxjs"; +import { filter } from "rxjs/operators"; + +export const alertService = { + onAlert, + success, + error, + info, + warn, + alert, + clear, +}; + +export const AlertType = { + Success: "Success", + Error: "Error", + Info: "Info", + Warning: "Warning", +}; + +const alertSubject = new Subject(); +const defaultId = "default-alert"; + +// enable subscribing to alerts observable +function onAlert(id = defaultId) { + return alertSubject.asObservable().pipe(filter((x) => x && x.id === id)); +} + +// convenience methods +function success(message, options) { + alert({ ...options, type: AlertType.Success, message }); +} + +function error(message, options) { + alert({ ...options, type: AlertType.Error, message }); +} + +function info(message, options) { + alert({ ...options, type: AlertType.Info, message }); +} + +function warn(message, options) { + alert({ ...options, type: AlertType.Warning, message }); +} + +// core alert method +function alert(alert) { + alert.id = alert.id || defaultId; + alert.autoClose = alert.autoClose === undefined ? true : alert.autoClose; + alertSubject.next(alert); +} + +// clear alerts +function clear(id = defaultId) { + alertSubject.next({ id }); +} diff --git a/services/index.js b/services/index.js new file mode 100644 index 0000000..16e7920 --- /dev/null +++ b/services/index.js @@ -0,0 +1 @@ +export * from "./alert.service"; diff --git a/styles/Components.module.css b/styles/Components.module.css index af18b95..e0831fe 100644 --- a/styles/Components.module.css +++ b/styles/Components.module.css @@ -2,27 +2,31 @@ min-height: 90vh; max-height: 90vh; max-width: 50vw; - left: 45vw; + left: 40vw; position: relative; overflow-y: scroll; display: flex; flex-wrap: wrap; } +.preview { + max-width: 50vw; + left: 0; +} + .borderKeeper { width: 132px !important; height: 132px !important; - padding: 4px; + padding: 6px; + margin: 12px; } .borderKeeper > span { display: inline-table !important; } -.userImage, -.borderImage { - position: relative; - left: 0; +.borderKeeper :nth-child(2) { + top: -8em; } .userImage { @@ -32,9 +36,10 @@ .borderImage { z-index: 3; - top: -256px !important; + transform: scale(1.22); } .selected { - border: 1px solid black; + border: 3px solid black; + z-index: 5; } diff --git a/styles/Home.module.css b/styles/Home.module.css index abbaaa5..d685ebf 100644 --- a/styles/Home.module.css +++ b/styles/Home.module.css @@ -16,7 +16,7 @@ position: absolute; display: flex; flex-direction: column; - justify-content: end; + justify-content: flex-end; align-items: flex-end; text-align: right; width: 50vw; diff --git a/styles/globals.css b/styles/globals.css index e5e2dcc..2e21b88 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -14,3 +14,17 @@ a { * { box-sizing: border-box; } + +.alert { + border: 1px black solid; + border-radius: 7%; + max-width: min-content; + padding: 4px; + background-color: lightgray; + position: relative; + left: 45vw; +} + +main { + display: flex; +} diff --git a/util/get_data.js b/util/get_data.js index 98bdbff..6188d7e 100644 --- a/util/get_data.js +++ b/util/get_data.js @@ -70,6 +70,8 @@ async function main() { const appid = border.appid; const name = border.community_item_data.item_name; + const community_item_type = border.community_item_type; + const filename_large = border.community_item_data.item_image_large; if (filename_large == "c05c0155855b74c28e0f6e9417d4afa3c99d76ef.png") { console.log(border); @@ -84,6 +86,7 @@ async function main() { borders.push({ name: `${name}-LARGE`, borderURL: `${borderURL}/${filename_large}`, + itemType: community_item_type, appInfo, }); } @@ -92,6 +95,7 @@ async function main() { borders.push({ name: `${name}-SMALL`, borderURL: `${borderURL}/${filename_small}`, + itemType: community_item_type, appInfo, }); } diff --git a/util/ingest.js b/util/ingest.js index 32b1f8f..81f95ae 100644 --- a/util/ingest.js +++ b/util/ingest.js @@ -32,6 +32,7 @@ let catalogue = async () => { id: 0, imageName: item, borderName: "Default", + itemType: 54, }, }); numAdded++; @@ -95,12 +96,14 @@ let download = async () => { appId: value.appInfo.appid, appName: value.appInfo.name, borderName: value.name, + itemType: value.itemType, }, update: { imageName: filename, appId: value.appInfo.appid, appName: value.appInfo.name, borderName: value.name, + itemType: value.itemType, }, where: { imageName: filename,