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) => (
+
+ ))}
+
+ );
+}
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 (
+
+ {
+ onSelect && onSelect(border.id);
+ }}
+ />
+ {
+ 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 (
-
- {
- onSelect(border.id);
- }}
- />
- {
- 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,