From 24478b7003b1a798767b22d87a1bb33adec2ea00 Mon Sep 17 00:00:00 2001
From: WCY-dt <834421194@qq.com>
Date: Wed, 1 May 2024 00:25:26 +0800
Subject: [PATCH] ADD: login, logout, notification
---
package-lock.json | 23 +++++++-
package.json | 7 ++-
src/components/content.tsx | 5 +-
src/components/header.tsx | 95 ++++++++++++++++++++-----------
src/popups/login.tsx | 54 ++++++++++++++++++
src/popups/notification.tsx | 36 ++++++++++++
src/routers/router.tsx | 26 +++++++--
src/services/collectionFetcher.ts | 2 +-
src/services/dataFetcher.ts | 2 +-
src/services/loginHandler.ts | 35 ++++++++++++
src/styles/header.css | 43 ++++++++++++++
src/styles/linkCard.css | 4 +-
src/styles/login.css | 67 ++++++++++++++++++++++
src/styles/notification.css | 18 ++++++
src/styles/popup.css | 16 ++++++
15 files changed, 383 insertions(+), 50 deletions(-)
create mode 100644 src/popups/login.tsx
create mode 100644 src/popups/notification.tsx
create mode 100644 src/services/loginHandler.ts
create mode 100644 src/styles/login.css
create mode 100644 src/styles/notification.css
create mode 100644 src/styles/popup.css
diff --git a/package-lock.json b/package-lock.json
index ff1c1cc..7be71d4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "mindclip",
- "version": "1.1.0",
+ "version": "2.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mindclip",
- "version": "1.1.0",
+ "version": "2.0.0",
"license": "GPL-3.0-only",
"dependencies": {
"react": "^18.2.0",
@@ -19,6 +19,7 @@
"@iconify/react": "^4.1.1",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
+ "cross-env": "^7.0.3",
"typescript": "^4.9.5"
}
},
@@ -5374,6 +5375,24 @@
"node": ">=10"
}
},
+ "node_modules/cross-env": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-7.0.3.tgz",
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "bin": {
+ "cross-env": "src/bin/cross-env.js",
+ "cross-env-shell": "src/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=10.14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"license": "MIT",
diff --git a/package.json b/package.json
index df9d3a4..705548e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "mindclip",
- "version": "1.1.0",
+ "version": "2.0.0",
"private": true,
"author": "ch3nyang",
"license": "GPL-3.0-only",
@@ -13,8 +13,8 @@
"react-scripts": "5.0.1"
},
"scripts": {
- "start": "react-scripts start",
- "build": "react-scripts build"
+ "start": "cross-env REACT_APP_API_URL=http://127.0.0.1:8787 react-scripts start",
+ "build": "cross-env REACT_APP_API_URL=https://api.mind.ch3nyang.top react-scripts build"
},
"eslintConfig": {
"extends": [
@@ -37,6 +37,7 @@
"@iconify/react": "^4.1.1",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
+ "cross-env": "^7.0.3",
"typescript": "^4.9.5"
}
}
diff --git a/src/components/content.tsx b/src/components/content.tsx
index 948b901..4f8add7 100644
--- a/src/components/content.tsx
+++ b/src/components/content.tsx
@@ -7,9 +7,12 @@ interface ContentProps {
routes: { [key: string]: string };
searchTerm: string;
setSearchTerm: (value: string) => void;
+ isLogedIn: boolean;
+ token: string;
+ setToken: (value: string) => void;
}
-function Content({ routes, searchTerm, setSearchTerm }: ContentProps) {
+function Content({ routes, searchTerm, setSearchTerm, isLogedIn, token, setToken }: ContentProps) {
return (
diff --git a/src/components/header.tsx b/src/components/header.tsx
index 41a0c3d..9d6a3e3 100644
--- a/src/components/header.tsx
+++ b/src/components/header.tsx
@@ -1,15 +1,22 @@
import { Icon } from '@iconify/react';
import React, { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
+import Login from '../popups/login';
import '../styles/header.css';
interface HeaderProps {
routes: { [key: string]: string };
searchTerm: string;
setSearchTerm: (value: string) => void;
+ isLogedIn: boolean;
+ setIsLogedIn: (value: boolean) => void;
+ token: string;
+ setToken: (value: string) => void;
+ message: string | null;
+ setMessage: (value: string | null) => void;
}
-function Header({ routes, searchTerm, setSearchTerm }: HeaderProps) {
+function Header({ routes, searchTerm, setSearchTerm, isLogedIn, setIsLogedIn, token, setToken, message, setMessage }: HeaderProps) {
const inputRef = useRef(null);
useEffect(() => {
localStorage.removeItem('colorMap');
@@ -25,40 +32,60 @@ function Header({ routes, searchTerm, setSearchTerm }: HeaderProps) {
const [isOpen, setIsOpen] = useState(false);
const toggleMenu = () => setIsOpen(!isOpen);
+ const [showLogin, setShowLogin] = useState(false);
+ const toggleLogin = () => {
+ setShowLogin(!showLogin);
+ };
+ const toggleLogout = () => {
+ setIsLogedIn(false);
+ setToken('');
+ localStorage.removeItem('token');
+ setMessage('Successfully logged out');
+ };
+
return (
-
+
)
}
diff --git a/src/popups/login.tsx b/src/popups/login.tsx
new file mode 100644
index 0000000..2f3a5f9
--- /dev/null
+++ b/src/popups/login.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { Icon } from '@iconify/react';
+import loginHandler from '../services/loginHandler';
+import '../styles/popup.css';
+import '../styles/login.css';
+
+interface LoginProps {
+ setShowLogin: (value: boolean) => void;
+ setIsLogedIn: (value: boolean) => void;
+ token: string;
+ setToken: (value: string) => void;
+ message: string | null;
+ setMessage: (value: string | null) => void;
+}
+
+function Login({ setShowLogin, setIsLogedIn, token, setToken, message, setMessage }: LoginProps) {
+ const tryLogin = async () => {
+ const username = (document.getElementById("username") as HTMLInputElement).value;
+ const password = (document.getElementById("password") as HTMLInputElement).value;
+ const loginResult = await loginHandler({ username, password, setToken });
+
+ console.log(loginResult);
+
+ if (loginResult === true) {
+ setShowLogin(false);
+ localStorage.setItem('token', token);
+ setIsLogedIn(true);
+ setMessage('Successfully logged in');
+ } else {
+ alert('Failed to login. Please try again.');
+ }
+ };
+
+ return (
+ <>
+
+
Login
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default Login;
\ No newline at end of file
diff --git a/src/popups/notification.tsx b/src/popups/notification.tsx
new file mode 100644
index 0000000..574b7a3
--- /dev/null
+++ b/src/popups/notification.tsx
@@ -0,0 +1,36 @@
+import React, { useState, useEffect } from 'react';
+import '../styles/notification.css';
+
+interface NotificationProps {
+ message: string | null;
+ setMessage: (value: string | null) => void;
+}
+
+function Notification({ message, setMessage }: NotificationProps) {
+ const [visible, setVisible] = useState(false);
+
+ useEffect(() => {
+ if (message !== null) {
+ setVisible(true);
+ const visibilityTimer = setTimeout(() => {
+ setVisible(false);
+ }, 2000);
+ const messageTimer = setTimeout(() => {
+ setMessage(null);
+ }, 4000);
+ return () => {
+ clearTimeout(visibilityTimer);
+ clearTimeout(messageTimer);
+ };
+ }
+ return;
+ }, [message, setMessage]);
+
+ return (
+
+ {message}
+
+ );
+}
+
+export default Notification;
\ No newline at end of file
diff --git a/src/routers/router.tsx b/src/routers/router.tsx
index 93f3b0d..6a1cd5f 100644
--- a/src/routers/router.tsx
+++ b/src/routers/router.tsx
@@ -5,20 +5,32 @@ import { fetchCollection } from '../services/collectionFetcher';
import Header from '../components/header';
import Content from '../components/content';
import Footer from '../components/footer';
+import Notification from '../popups/notification';
import '../styles/loading.css';
function Router() {
const [searchTerm, setSearchTerm] = useState('');
const [isLoading, setIsLoading] = useState(true);
+ const [isLogedIn, setIsLogedIn] = useState(false);
+ const [token, setToken] = useState('');
+ const [message, setMessage] = useState(null);
const [routes, setRoutes] = useState({});
+
useEffect(() => {
setIsLoading(true);
fetchCollection(setRoutes)
.finally(() => setIsLoading(false));
}, []);
+ useEffect(() => {
+ if (localStorage.getItem('token')) {
+ setIsLogedIn(true);
+ setToken(localStorage.getItem('token') as string);
+ }
+ }, []);
+
if (isLoading) {
return (
@@ -26,14 +38,16 @@ function Router() {
}
return (
-
-
-
-
+ <>
+
+
-
-
+
+
+
+
+ >
);
}
diff --git a/src/services/collectionFetcher.ts b/src/services/collectionFetcher.ts
index e3037d2..74adf8f 100644
--- a/src/services/collectionFetcher.ts
+++ b/src/services/collectionFetcher.ts
@@ -1,6 +1,6 @@
export function fetchCollection(setRoutes: (value: {}) => void) {
return new Promise(async (resolve, reject) => {
- const api = "https://api.mind.ch3nyang.top";
+ const api = process.env.REACT_APP_API_URL;
let url = `${api}/collection`;
try {
diff --git a/src/services/dataFetcher.ts b/src/services/dataFetcher.ts
index 3a448b5..1787120 100644
--- a/src/services/dataFetcher.ts
+++ b/src/services/dataFetcher.ts
@@ -6,7 +6,7 @@ export function fetchAndFilterData(
setClusters: (value: ClusterProps[]) => void
) {
return new Promise(async (resolve, reject) => {
- const api = "https://api.mind.ch3nyang.top";
+ const api = process.env.REACT_APP_API_URL;
let url = `${api}/card?collection=${dataKey}`;
if (selectedCategory) {
url += `&category=${selectedCategory}`;
diff --git a/src/services/loginHandler.ts b/src/services/loginHandler.ts
new file mode 100644
index 0000000..ce50418
--- /dev/null
+++ b/src/services/loginHandler.ts
@@ -0,0 +1,35 @@
+interface LoginHandlerProps {
+ username: string;
+ password: string;
+ setToken: (value: string) => void;
+}
+
+async function loginHandler({ username, password, setToken }: LoginHandlerProps): Promise {
+ const api = process.env.REACT_APP_API_URL;
+ let url = `${api}/login`;
+
+ try {
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'text/plain',
+ },
+ body: JSON.stringify({ user: username, pass: password }),
+ });
+
+ if (!response.ok) {
+ return false;
+ }
+
+ const data = await response.json();
+ let token = data.token;
+ setToken(token);
+ // console.log('Token:', token);
+ return true;
+ } catch (error) {
+ console.error('Error:', error);
+ return false;
+ }
+}
+
+export default loginHandler;
\ No newline at end of file
diff --git a/src/styles/header.css b/src/styles/header.css
index 649f1dc..e49d6a8 100644
--- a/src/styles/header.css
+++ b/src/styles/header.css
@@ -12,6 +12,20 @@
background-color: #ffffff;
}
+.App-nav-left {
+ height: 100%;
+}
+
+.App-nav-right {
+ height: 100%;
+ width: auto;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+}
+
.App-nav-title {
display: flex;
align-items: center;
@@ -98,6 +112,26 @@
outline: none;
}
+.App-nav-login {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ background-color: #333333;
+ color: white;
+ border: none;
+ border-radius: 5px;
+ padding: 10px;
+ cursor: pointer;
+
+ font-size: 1.5em;
+}
+
+.App-nav-login:hover {
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
+ transition: box-shadow 0.1s;
+}
+
.App-nav-menu {
display: none
}
@@ -143,6 +177,14 @@
padding: 10px 0;
}
+ .App-nav-right {
+ width: 100%;
+ }
+
+ .App-nav-login {
+ display: none;
+ }
+
.App-nav-search {
display: none;
}
@@ -179,6 +221,7 @@
left: 20px;
background-color: transparent;
+ color: black;
border: none;
cursor: pointer;
diff --git a/src/styles/linkCard.css b/src/styles/linkCard.css
index c1fa153..dc6a28d 100644
--- a/src/styles/linkCard.css
+++ b/src/styles/linkCard.css
@@ -14,7 +14,7 @@
padding: 0;
- border-radius: 1rem;
+ border-radius: 5px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
@@ -65,7 +65,7 @@
word-break: break-all;
text-transform: uppercase;
- border-top-left-radius: 1rem;
+ border-top-left-radius: 5px;
}
.link-card-icon-img {
diff --git a/src/styles/login.css b/src/styles/login.css
new file mode 100644
index 0000000..9d7a1b0
--- /dev/null
+++ b/src/styles/login.css
@@ -0,0 +1,67 @@
+.Login-title {
+ font-size: 2rem;
+ text-transform: capitalize;
+ font-weight: 900;
+}
+
+.Login-input {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: auto 1fr;
+
+ align-items: center;
+ justify-content: center;
+}
+
+.Login-input input {
+ font-size: 1rem;
+ padding: 0.5rem;
+
+ border: none;
+ border-radius: 5px;
+ background-color: #f0f0f0;
+
+ border: 2px solid transparent;
+}
+
+.Login-input input:focus {
+ outline: none;
+ border-bottom: 2px solid #333333;
+}
+
+.Login-input label {
+ font-size: 1.2rem;
+ font-weight: 700;
+}
+
+.Login-button {
+ font-size: 1rem;
+ padding: 0.5rem;
+ background-color: #333333;
+ color: #fff;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ width: 50%;
+ margin: 0 auto;
+ text-transform: uppercase;
+ font-weight: 700;
+}
+
+.Login-button:hover {
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
+ transition: box-shadow 0.1s;
+}
+
+.Login-close {
+ position: absolute;
+ top: 0;
+ right: 0;
+ font-size: 2rem;
+ padding: 0.5rem;
+ cursor: pointer;
+
+ color: #333;
+ background-color: #fff;
+ border: none;
+}
\ No newline at end of file
diff --git a/src/styles/notification.css b/src/styles/notification.css
new file mode 100644
index 0000000..52082c4
--- /dev/null
+++ b/src/styles/notification.css
@@ -0,0 +1,18 @@
+.notification {
+ position: fixed;
+ bottom: 20px;
+ left: 50%;
+ transform: translateX(-50%);
+ padding: 20px;
+ background-color: black;
+ color: white;
+ border-radius: 5px;
+ opacity: 0;
+ transition: opacity 2s;
+ z-index: 200;
+ font-weight: 700;
+}
+
+.notification.show {
+ opacity: 1;
+}
\ No newline at end of file
diff --git a/src/styles/popup.css b/src/styles/popup.css
new file mode 100644
index 0000000..2b6dce9
--- /dev/null
+++ b/src/styles/popup.css
@@ -0,0 +1,16 @@
+.Popup {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background-color: white;
+ padding: 20px;
+ border-radius: 5px;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
+
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+
+ z-index: 10;
+}
\ No newline at end of file