diff --git a/client/.gitignore b/client/.gitignore
index d3614a66..20d447a7 100644
--- a/client/.gitignore
+++ b/client/.gitignore
@@ -14,6 +14,7 @@ dist-ssr
.env
*.css.map
+*.css
# Editor directories and files
.vscode/*
diff --git a/client/package-lock.json b/client/package-lock.json
index e61eba51..5f74488a 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -8,17 +8,15 @@
"name": "client",
"version": "0.0.0",
"dependencies": {
- "@reduxjs/toolkit": "^2.0.1",
-
"@heroicons/react": "^2.0.18",
-
+ "@reduxjs/toolkit": "^2.0.1",
"axios": "^1.6.2",
"normalize.css": "^8.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
-
"react-redux": "^9.0.2",
"react-router-dom": "^6.21.0",
+ "react-toastify": "^9.1.3",
"sass": "^1.69.5"
},
"devDependencies": {
@@ -1288,6 +1286,14 @@
"node": ">= 6"
}
},
+ "node_modules/clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3154,6 +3160,18 @@
"react-dom": ">=16.8"
}
},
+ "node_modules/react-toastify": {
+ "version": "9.1.3",
+ "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz",
+ "integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==",
+ "dependencies": {
+ "clsx": "^1.1.1"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
diff --git a/client/package.json b/client/package.json
index c5c6a20a..78f1aeeb 100644
--- a/client/package.json
+++ b/client/package.json
@@ -4,21 +4,21 @@
"version": "0.0.0",
"type": "module",
"scripts": {
- "dev": "vite",
+ "dev": "vite --host",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
- "@reduxjs/toolkit": "^2.0.1",
"@heroicons/react": "^2.0.18",
+ "@reduxjs/toolkit": "^2.0.1",
"axios": "^1.6.2",
"normalize.css": "^8.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
-
"react-redux": "^9.0.2",
"react-router-dom": "^6.21.0",
+ "react-toastify": "^9.1.3",
"sass": "^1.69.5"
},
"devDependencies": {
diff --git a/client/src/App.jsx b/client/src/App.jsx
index 042e44ef..470df060 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -4,11 +4,21 @@ import "./theme.scss";
import "./assets/styles/global/global.scss";
// Import Routes
import Routes from "./routes";
+// Import Store
+import store from "./redux/store";
+// Import Provider
+import { Provider } from "react-redux";
+// Import Toastify
+import { ToastContainer } from "react-toastify";
+import "react-toastify/dist/ReactToastify.css";
function App() {
return (
<>
-
+
+
+
+
>
);
}
diff --git a/client/src/assets/styles/_vars.scss b/client/src/assets/styles/_vars.scss
index acdc3eb9..7186fd13 100644
--- a/client/src/assets/styles/_vars.scss
+++ b/client/src/assets/styles/_vars.scss
@@ -1,5 +1,11 @@
$--grey-800: #1F2A37;
+$--grey-700: #374151;
+$--grey-600: #4b5563;
$--grey-300: #D1D5DB;
+$--Teal-900: #014451;
$--Teal-500: #0694A2;
$--Red-700: #C81E1E;
-$--Yellow-500: #C27803;
\ No newline at end of file
+$--green-700: #046C4E;
+$--Yellow-500: #C27803;
+$--Primary-900: #0e0218;
+$--text-color: #e2efee;
\ No newline at end of file
diff --git a/client/src/assets/styles/components/Alert.scss b/client/src/assets/styles/components/Alert.scss
index 156bc8a7..62c53daf 100644
--- a/client/src/assets/styles/components/Alert.scss
+++ b/client/src/assets/styles/components/Alert.scss
@@ -12,7 +12,7 @@
padding: 15px 15px;
justify-content: center;
- align-items: end;
+ align-items: end;
gap: 10px;
}
diff --git a/client/src/assets/styles/components/Button.scss b/client/src/assets/styles/components/Button.scss
index b21570cb..9af0126b 100644
--- a/client/src/assets/styles/components/Button.scss
+++ b/client/src/assets/styles/components/Button.scss
@@ -3,6 +3,11 @@
border: none;
padding: 12px 20px;
border-radius: 6px;
+ cursor: pointer;
+ color: inherit;
+ font-size: 19px;
+ font-weight: 500;
+ text-align: center;
a {
color: inherit;
@@ -20,5 +25,16 @@
color: var(--grey-50);
}
+ &--medium {
+ a {
+ font-size: 1rem;
+ }
+ }
+
+ &--success {
+ background-color: var(--green-400);
+ color: var(--grey-900);
+ }
+
// TODO: Add more button styles and maybe sizes
}
diff --git a/client/src/assets/styles/components/FancyBlobs.scss b/client/src/assets/styles/components/FancyBlobs.scss
index 7d2054bc..5ebf7ded 100644
--- a/client/src/assets/styles/components/FancyBlobs.scss
+++ b/client/src/assets/styles/components/FancyBlobs.scss
@@ -6,6 +6,8 @@
top: 20vh;
left: 0;
z-index: -1;
+ --animation-duration: 8s;
+ --max-distance: 300px;
> * {
position: absolute;
@@ -18,7 +20,7 @@
fill: linear-gradient(82deg, #8900c9 38.7%, #fcf 76.51%);
filter: blur(150px);
top: -167px;
- animation: BlobMove1 10s ease-in-out infinite forwards;
+ animation: BlobMove1 var(--animation-duration) ease-in-out infinite forwards;
}
.Blob-2 {
@@ -26,7 +28,7 @@
filter: blur(125px);
top: 66px;
left: -71px;
- animation: BlobMove2 10s ease-in-out infinite forwards;
+ animation: BlobMove2 var(--animation-duration) ease-in-out infinite forwards;
}
.Blob-3 {
@@ -37,7 +39,7 @@
filter: blur(150px);
top: 460px;
left: 0;
- animation: BlobMove3 10s ease-in-out infinite forwards;
+ animation: BlobMove3 var(--animation-duration) ease-in-out infinite forwards;
}
.Blob-4 {
@@ -48,7 +50,7 @@
filter: blur(150px);
top: 503px;
left: 150px;
- animation: BlobMove4 10s ease-in-out infinite forwards;
+ animation: BlobMove4 var(--animation-duration) ease-in-out infinite forwards;
}
}
@@ -105,7 +107,7 @@
transform: translate(0, 50px);
}
75% {
- transform: translate(-100px, 0);
+ transform: translate(-var(--max-distance), 0);
}
100% {
transform: translate(0, 0);
diff --git a/client/src/assets/styles/components/Footer.scss b/client/src/assets/styles/components/Footer.scss
index 824625cf..37f62e3b 100644
--- a/client/src/assets/styles/components/Footer.scss
+++ b/client/src/assets/styles/components/Footer.scss
@@ -1,8 +1,14 @@
.Footer {
padding: 2rem 20px;
+ margin-bottom: 0;
text-align: center;
position: relative;
box-sizing: content-box;
+ margin-bottom: 50px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 20px;
&::before {
content: "";
@@ -23,6 +29,7 @@
}
a {
+ border-radius: 5px;
padding: 5px;
background-color: var(--primary-50);
color: var(--grey-900);
diff --git a/client/src/assets/styles/components/Inputs.scss b/client/src/assets/styles/components/Inputs.scss
new file mode 100644
index 00000000..448be953
--- /dev/null
+++ b/client/src/assets/styles/components/Inputs.scss
@@ -0,0 +1,34 @@
+.input {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ color: var(--grey-100);
+ font-size: 1rem;
+ direction: rtl;
+ width: 100%;
+
+ .radio-buttons {
+ display: flex;
+ flex-direction: row;
+ gap: 0.5rem;
+ width: 100%;
+ margin: 0.5rem 0;
+ }
+
+ input[type="text"],
+ input[type="password"],
+ input[type="email"],
+ input[type="number"],
+ input[type="tel"],
+ input[type="date"],
+ input[type="time"] {
+ width: 100%;
+ outline: none;
+ border-radius: 4px;
+ border: 1px solid var(--grey-600, #4b5563);
+ background: var(--grey-700, #374151);
+ padding: 0 4px;
+ line-height: 1rem;
+ color: var(--grey-100);
+ }
+}
diff --git a/client/src/assets/styles/components/infoBox.scss b/client/src/assets/styles/components/infoBox.scss
new file mode 100644
index 00000000..03986681
--- /dev/null
+++ b/client/src/assets/styles/components/infoBox.scss
@@ -0,0 +1,35 @@
+.info-box {
+ height: 97px;
+ flex-shrink: 0;
+ border-radius: 12px;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+ align-items: end;
+
+ padding-right: 2rem;
+
+ &.narrow {
+ width: 40%;
+ }
+
+ &.wide {
+ width: 90%;
+ }
+
+ &.dark {
+
+ background: var(--simnle-grid, linear-gradient(113deg, #1F2A37 6.36%, rgba(31, 42, 55, 0.00) 108.22%));
+ }
+
+ &.purple {
+ background: var(--pur-grid, linear-gradient(115deg, #B773E9 2.95%, #5B1092 95.69%));
+
+ }
+
+ &.colorfull {
+ background: linear-gradient(96deg, #650DA6 -11.62%, #650DA6 -11.61%, #DF1846 119.72%);
+ box-shadow: 4px 5px 103.7px 0px var(--Primary-800, #2B0547);
+ }
+}
\ No newline at end of file
diff --git a/client/src/assets/styles/components/logIn.css b/client/src/assets/styles/components/logIn.css
deleted file mode 100644
index 133f3432..00000000
--- a/client/src/assets/styles/components/logIn.css
+++ /dev/null
@@ -1,64 +0,0 @@
-.hero {
- display: flex;
- width: 22.5rem;
- height: 40rem;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- margin: auto;
- padding: 1.75rem 0.25rem;
- gap: 1.1875rem;
-}
-
-.card {
- background-color: #1f2a37;
- border-radius: 1.875rem;
- display: flex;
- width: 16.9375rem;
- padding: 1.0625rem 0rem;
- margin-top: 3rem;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- gap: 0.875rem;
-}
-.card .input-field {
- display: flex;
- width: 14.16319rem;
- padding: 0.125rem 0.75rem;
- justify-content: flex-end;
- align-items: center;
- align-content: center;
- gap: 0rem 9.25rem;
- flex-wrap: wrap;
-}
-.card .input-field input {
- background: #374151;
- margin-block: 0.3rem;
- border: 1px solid #4b5563;
- border-radius: 0.25rem;
- width: 12.66319rem;
- height: 1.75rem;
- flex-shrink: 0;
-}
-
-.no-account {
- color: #0694A2;
- font-family: "Noto Kufi Arabic";
- margin-bottom: 1rem;
-}
-
-button {
- color: #e2efee;
- font-family: "Noto Kufi Arabic";
- background: #046C4E;
- border: 0px solid;
- border-radius: 0.3125rem;
- display: flex;
- width: 7.25rem;
- height: 1.875rem;
- padding: 0.8125rem 0.625rem;
- justify-content: center;
- align-items: center;
- gap: 1.25rem;
-}/*# sourceMappingURL=logIn.css.map */
\ No newline at end of file
diff --git a/client/src/assets/styles/components/logIn.scss b/client/src/assets/styles/components/logIn.scss
deleted file mode 100644
index 7b4053b1..00000000
--- a/client/src/assets/styles/components/logIn.scss
+++ /dev/null
@@ -1,74 +0,0 @@
-$Teal-900: #014451;
-$Teal-500: #0694A2;
-$grey-800: #1f2a37;
-$grey-700: #374151;
-$grey-600: #4b5563;
-$green-700: #046C4E;
-$Primary-900: #0e0218;
-$text-color: #e2efee;
-$font: 'Noto Kufi Arabic';
-
-.hero {
- display: flex;
- width: 22.5rem;
- height: 40rem;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- margin: auto;
- padding: 1.75rem 0.25rem;
- gap: 1.1875rem;
-}
-
-.card {
- background-color: $grey-800;
- border-radius: 1.875rem;
- display: flex;
- width: 16.9375rem;
- padding: 1.0625rem 0rem;
- margin-top: 3rem;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- gap: 0.875rem;
- .input-field {
- display: flex;
- width: 14.16319rem;
- padding: 0.125rem 0.75rem;
- justify-content: flex-end;
- align-items: center;
- align-content: center;
- gap: 0rem 9.25rem;
- flex-wrap: wrap;
- input {
- background: $grey-700;
- margin-block: 0.3rem;
- border: 1px solid $grey-600;
- border-radius: 0.25rem;
- width: 12.66319rem;
- height: 1.75rem;
- flex-shrink: 0;
- }
- }
-}
-
-.no-account {
- color: $Teal-500;
- font-family: $font;
- margin-bottom: 1rem;
-}
-
-button {
- color: $text-color;
- font-family: $font;
- background: $green-700;
- border: 0px solid;
- border-radius: 0.3125rem;
- display: flex;
- width: 7.25rem;
- height: 1.875rem;
- padding: 0.8125rem 0.625rem;
- justify-content: center;
- align-items: center;
- gap: 1.25rem;
-}
diff --git a/client/src/assets/styles/components/signUp.css b/client/src/assets/styles/components/signUp.css
deleted file mode 100644
index e40032e3..00000000
--- a/client/src/assets/styles/components/signUp.css
+++ /dev/null
@@ -1,87 +0,0 @@
-.hero {
- display: flex;
- width: 22.5rem;
- height: 93.375rem;
- flex-direction: column;
- align-items: center;
- margin: auto;
- padding: 1.75rem 0.25rem;
- gap: 1.1875rem;
-}
-
-.container {
- background: #014451;
- border-radius: 1.25rem;
- display: flex;
- width: 19rem;
- padding: 1.5rem 0.4375rem;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- gap: 1.0625rem;
-}
-
-.card {
- background-color: #1f2a37;
- border-radius: 1.875rem;
- display: flex;
- width: 16.9375rem;
- padding: 1.0625rem 0rem;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- gap: 0.875rem;
-}
-.card .input-field {
- display: flex;
- width: 14.16319rem;
- padding: 0.125rem 0.75rem;
- justify-content: flex-end;
- align-items: center;
- align-content: center;
- gap: 0rem 9.25rem;
- flex-wrap: wrap;
-}
-.card .input-field input {
- background: #374151;
- margin-block: 0.3rem;
- border: 1px solid #4b5563;
- border-radius: 0.25rem;
- width: 12.66319rem;
- height: 1.75rem;
- flex-shrink: 0;
-}
-.card .radio-field {
- display: flex;
- width: 14.16319rem;
- padding: 0.5625rem 0.75rem;
- flex-direction: column;
- justify-content: flex-end;
- align-items: flex-end;
- flex-wrap: wrap;
- gap: 0.0625rem;
-}
-.card .radio-field .radio-buttons {
- display: flex;
- padding: 0.3125rem 0.5rem;
- margin: auto;
- gap: 3.1875rem;
-}
-.card .radio-field .radio-buttons small input {
- margin: 0.5rem;
-}
-
-button {
- color: #e2efee;
- font-family: "Noto Kufi Arabic";
- background: #0e0218;
- border: 0px solid;
- border-radius: 0.3125rem;
- display: flex;
- width: 12.5rem;
- height: 3.125rem;
- padding: 0.8125rem 0.6875rem 0.8125rem 0.625rem;
- justify-content: center;
- align-items: center;
- gap: 2.5rem;
-}/*# sourceMappingURL=signUp.css.map */
\ No newline at end of file
diff --git a/client/src/assets/styles/components/signUp.scss b/client/src/assets/styles/components/signUp.scss
deleted file mode 100644
index 9705ee79..00000000
--- a/client/src/assets/styles/components/signUp.scss
+++ /dev/null
@@ -1,97 +0,0 @@
-$Teal-900: #014451;
-$grey-800: #1f2a37;
-$grey-700: #374151;
-$grey-600: #4b5563;
-$Primary-900: #0e0218;
-$text-color: #e2efee;
-$font: 'Noto Kufi Arabic';
-
-.hero {
- display: flex;
- width: 22.5rem;
- height: 93.375rem;
- flex-direction: column;
- align-items: center;
- margin: auto;
- padding: 1.75rem 0.25rem;
- gap: 1.1875rem;
-}
-
-.container {
- background: $Teal-900;
- border-radius: 1.25rem;
- display: flex;
- width: 19rem;
- padding: 1.5rem 0.4375rem;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- gap: 1.0625rem;
-}
-
-.card {
- background-color: $grey-800;
- border-radius: 1.875rem;
- display: flex;
- width: 16.9375rem;
- padding: 1.0625rem 0rem;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- gap: 0.875rem;
- .input-field {
- display: flex;
- width: 14.16319rem;
- padding: 0.125rem 0.75rem;
- justify-content: flex-end;
- align-items: center;
- align-content: center;
- gap: 0rem 9.25rem;
- flex-wrap: wrap;
- input {
- background: $grey-700;
- margin-block: 0.3rem;
- border: 1px solid $grey-600;
- border-radius: 0.25rem;
- width: 12.66319rem;
- height: 1.75rem;
- flex-shrink: 0;
- }
- }
-
- .radio-field {
- display: flex;
- width: 14.16319rem;
- padding: 0.5625rem 0.75rem;
- flex-direction: column;
- justify-content: flex-end;
- align-items: flex-end;
- flex-wrap: wrap;
- gap: 0.0625rem;
- .radio-buttons {
- display: flex;
- padding: 0.3125rem 0.5rem;
- margin: auto;
- gap: 3.1875rem;
- small input {
- margin: 0.5rem;
- }
- }
- }
-}
-
-
-button {
- color: $text-color;
- font-family: $font;
- background: $Primary-900;
- border: 0px solid;
- border-radius: 0.3125rem;
- display: flex;
- width: 12.5rem;
- height: 3.125rem;
- padding: 0.8125rem 0.6875rem 0.8125rem 0.625rem;
- justify-content: center;
- align-items: center;
- gap: 2.5rem;
-}
diff --git a/client/src/assets/styles/global/global.scss b/client/src/assets/styles/global/global.scss
index 7fee132f..2ddba2b7 100644
--- a/client/src/assets/styles/global/global.scss
+++ b/client/src/assets/styles/global/global.scss
@@ -10,11 +10,13 @@
body {
font-family: var(--font-primary);
+ font-size: var(--font-base-size);
background-color: var(--bg);
- color: var(--grey-50);
+ color: var(--text-color);
+ overflow-x: hidden;
}
a {
- color: var(--grey-50);
+ color: var(--text-color);
text-decoration: none;
-}
+}
\ No newline at end of file
diff --git a/client/src/components/common/Alerts.jsx b/client/src/components/common/Alerts.jsx
index 1d25b7d2..21657456 100644
--- a/client/src/components/common/Alerts.jsx
+++ b/client/src/components/common/Alerts.jsx
@@ -24,7 +24,7 @@ const Alert = ({ title, info, buttontext, Onclick, showRightBox, color }) => {
className={
showRightBox ? "right-box " + color : "right-box hide " + color
}
- />
+ / >
{info}
diff --git a/client/src/components/common/Button.jsx b/client/src/components/common/Button.jsx
index 0d501194..861f338c 100644
--- a/client/src/components/common/Button.jsx
+++ b/client/src/components/common/Button.jsx
@@ -8,9 +8,17 @@ export default function Button(props) {
const { children, className, linkTo, ...rest } = props;
return (
-
+ <>
+ {linkTo ? (
+
+ {children}
+
+ ) : (
+
+ )}
+ >
);
}
diff --git a/client/src/components/common/InfoBox.jsx b/client/src/components/common/InfoBox.jsx
new file mode 100644
index 00000000..62d9420a
--- /dev/null
+++ b/client/src/components/common/InfoBox.jsx
@@ -0,0 +1,12 @@
+import React from "react";
+import "../../assets/styles/components/infoBox.scss";
+const InfoBox = ({ title, value, width = "narrow", color = "dark" }) => {
+ return (
+
+ );
+};
+
+export default InfoBox;
diff --git a/client/src/components/common/Inputs.jsx b/client/src/components/common/Inputs.jsx
new file mode 100644
index 00000000..5e4c69e8
--- /dev/null
+++ b/client/src/components/common/Inputs.jsx
@@ -0,0 +1,67 @@
+import "./../../assets/styles/components/Inputs.scss";
+
+import PropTypes from "prop-types";
+
+function TextInput({
+ label,
+ type,
+ name,
+ value,
+ onChange,
+ placeholder,
+ required,
+}) {
+ return (
+
+ );
+}
+TextInput.propTypes = {
+ label: PropTypes.string.isRequired,
+ type: PropTypes.string,
+ name: PropTypes.string,
+ value: PropTypes.string,
+ onChange: PropTypes.func,
+ placeholder: PropTypes.string,
+ required: PropTypes.bool,
+};
+function RadioInput({ label, name, required, valuesArr, onChange }) {
+ return (
+
+ );
+}
+RadioInput.propTypes = {
+ label: PropTypes.string.isRequired,
+ name: PropTypes.string,
+ valuesArr: PropTypes.array,
+ onChange: PropTypes.func,
+ required: PropTypes.bool,
+};
+
+export { RadioInput };
+export default TextInput;
diff --git a/client/src/components/common/PageTitle.jsx b/client/src/components/common/PageTitle.jsx
new file mode 100644
index 00000000..3bc8069e
--- /dev/null
+++ b/client/src/components/common/PageTitle.jsx
@@ -0,0 +1,9 @@
+import React from 'react'
+
+const PageTitle = ({title}) => {
+ return (
+ {title}
+ )
+}
+
+export default PageTitle
\ No newline at end of file
diff --git a/client/src/components/landingpage/LandingPage.jsx b/client/src/components/landingpage/LandingPage.jsx
index 478f5f19..0203440c 100644
--- a/client/src/components/landingpage/LandingPage.jsx
+++ b/client/src/components/landingpage/LandingPage.jsx
@@ -1,21 +1,84 @@
-import Nav from "../common/nav";
import Button from "../common/Button";
-import Footer from "../common/Footer";
import FancyBlobs from "./FancyBlobs";
+import "./LandingPage.scss";
+
+import PageTitle from "../common/PageTitle";
+
+import {
+ FolderIcon,
+ AcademicCapIcon,
+ PresentationChartLineIcon,
+ PencilSquareIcon,
+ BanknotesIcon,
+ FlagIcon,
+ BellAlertIcon,
+} from "@heroicons/react/24/solid";
+
+const features = [
+ {
+ icon: ,
+ text: "إدارة الغياب والاشتراك الاسبوعي",
+ },
+ {
+ icon: ,
+ text: "تقييم الأفراد ووضع النتائج",
+ },
+ {
+ icon: ,
+ text: "وضع الاحصاءيات والتقارير",
+ },
+ {
+ icon: ,
+ text: "تسجيل المواضيع والانشطة المتنوعة",
+ },
+ {
+ icon: ,
+ text: "إدارة الماليات والخزنة",
+ },
+ {
+ icon: ,
+ text: "إنشاء التقارير وتسجيل مجالس الشرف",
+ },
+ {
+ icon: ,
+ text: "إرسال التنبيهات والاشعارات للقادة",
+ },
+];
+
export default function LandingPage() {
return (
- <>
-
-
-
-
-
- >
+
+
+
+ مميزات
+ {features.map((feature, index) => (
+
+
{feature.icon}
+
{feature.text}
+
+ ))}
+
+
+
+
);
}
diff --git a/client/src/components/landingpage/LandingPage.scss b/client/src/components/landingpage/LandingPage.scss
new file mode 100644
index 00000000..f7d2be7d
--- /dev/null
+++ b/client/src/components/landingpage/LandingPage.scss
@@ -0,0 +1,72 @@
+.landing_page {
+ --icon-size: 32px;
+
+ .hero {
+ padding-block: 60px;
+
+ h1 {
+ text-align: center;
+ line-height: 1.2;
+ margin-bottom: 30px;
+ }
+
+ p {
+ text-align: center;
+ margin-bottom: 30px;
+ padding-inline: 5px;
+ line-height: 1.5;
+ }
+ .btn__container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 20px;
+
+ > * {
+ min-width: 70%;
+ }
+ }
+ }
+
+ .features {
+ padding-block: 60px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 60px;
+
+ .feature {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 20px;
+ position: relative;
+
+ &:nth-child(even)::before {
+ content: "";
+ position: absolute;
+ display: block;
+ width: 100%;
+ height: 100%;
+ background-color: var(--primary-600);
+ z-index: -1;
+ border-radius: 50%;
+ filter: blur(70px);
+ }
+
+ .icon {
+ color: var(--primary-100);
+ > * {
+ width: var(--icon-size);
+ height: var(--icon-size);
+ }
+ }
+
+ p {
+ max-width: 80%;
+ text-align: center;
+ }
+ }
+ }
+}
diff --git a/client/src/components/layout/PublicLayout.jsx b/client/src/components/layout/PublicLayout.jsx
new file mode 100644
index 00000000..37afc50e
--- /dev/null
+++ b/client/src/components/layout/PublicLayout.jsx
@@ -0,0 +1,14 @@
+import { Outlet } from "react-router-dom";
+
+import Nav from "../common/nav";
+import Footer from "../common/Footer";
+
+export default function PublicLayout() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/login/logIn.jsx b/client/src/components/login/logIn.jsx
new file mode 100644
index 00000000..4b98d8ff
--- /dev/null
+++ b/client/src/components/login/logIn.jsx
@@ -0,0 +1,74 @@
+import { useState, useEffect } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { Link, useNavigate } from "react-router-dom";
+import { toast } from "react-toastify";
+import Button from "../common/Button";
+import TextInput from "../common/Inputs";
+import "./logIn.scss";
+import { useLoginMutation } from "../../redux/slices/usersApiSlice";
+import { setCredentials } from "../../redux/slices/authSlice";
+
+export default function LogIn() {
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+
+ const [login, { isLoading, error }] = useLoginMutation();
+
+ const { userInfo } = useSelector((state) => state.auth);
+
+ useEffect(() => {
+ if (userInfo) {
+ /* TODO: Add later the home page not the landing page */
+ navigate("/");
+ }
+ }, [navigate, userInfo]);
+
+ const submitHandler = async (e) => {
+ e.preventDefault();
+ try {
+ const res = await login({ email, password }).unwrap();
+ dispatch(setCredentials({ ...res?.data }));
+ navigate("/");
+ } catch (err) {
+ toast.error(err?.data?.message || err.error);
+ console.error(err);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/client/src/components/login/logIn.scss b/client/src/components/login/logIn.scss
new file mode 100644
index 00000000..9af96fbb
--- /dev/null
+++ b/client/src/components/login/logIn.scss
@@ -0,0 +1,51 @@
+.login {
+ .hero {
+ display: flex;
+ width: 22.5rem;
+ height: 40rem;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ margin: auto;
+ padding: 1.75rem 0.25rem;
+ gap: 1.1875rem;
+ }
+
+ .card {
+ background-color: var(--grey-800);
+ border-radius: 1.875rem;
+ display: flex;
+ padding: 1.0625rem 0rem;
+ margin-top: 3rem;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 1rem;
+ padding: 3rem 1rem;
+
+ .input-field {
+ display: flex;
+ width: 14.16319rem;
+ padding: 0.125rem 0.75rem;
+ justify-content: flex-end;
+ align-items: center;
+ align-content: center;
+ gap: 0rem 9.25rem;
+ flex-wrap: wrap;
+ input {
+ background: var(--grey-700);
+ margin-block: 0.3rem;
+ border: 1px solid var(--grey-600);
+ border-radius: 0.25rem;
+ width: 12.66319rem;
+ height: 1.75rem;
+ flex-shrink: 0;
+ }
+ }
+ }
+
+ .no-account {
+ color: var(--Teal-500);
+ margin-bottom: 1rem;
+ }
+}
diff --git a/client/src/components/module_OR_PAGE/logIn.jsx b/client/src/components/module_OR_PAGE/logIn.jsx
deleted file mode 100644
index 65533d91..00000000
--- a/client/src/components/module_OR_PAGE/logIn.jsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import '../../assets/styles/components/logIn.css'
-
-export default function LogIn() {
- return (
-
- )
-}
diff --git a/client/src/components/module_OR_PAGE/signUp.jsx b/client/src/components/module_OR_PAGE/signUp.jsx
deleted file mode 100644
index 8316d4fb..00000000
--- a/client/src/components/module_OR_PAGE/signUp.jsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import '../../assets/styles/components/signUp.css'
-
-export default function SignUp() {
- return (
-
- )
-}
diff --git a/client/src/components/signup/signUp.jsx b/client/src/components/signup/signUp.jsx
new file mode 100644
index 00000000..5df44a01
--- /dev/null
+++ b/client/src/components/signup/signUp.jsx
@@ -0,0 +1,160 @@
+import { useState, useEffect } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { Link, useNavigate } from "react-router-dom";
+import { toast } from "react-toastify";
+import Button from "../common/Button";
+import TextInput, { RadioInput } from "../common/Inputs";
+import "./signUp.scss";
+import { useSignupMutation } from "../../redux/slices/usersApiSlice";
+import { setCredentials } from "../../redux/slices/authSlice";
+
+export default function SignUp() {
+ const [firstName, setFirstName] = useState("");
+ const [lastName, setLastName] = useState("");
+ const [middleName, setMiddleName] = useState("");
+ const [gender, setGender] = useState("");
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [rePassword, setRePassword] = useState("");
+ const [phone, setPhone] = useState("");
+
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+
+ const [signup, { isLoading, error }] = useSignupMutation();
+
+ const { userInfo } = useSelector((state) => state.auth);
+
+ useEffect(() => {
+ if (userInfo) {
+ /* TODO: Add later the home page not the landing page */
+ navigate("/");
+ }
+ }, [navigate, userInfo]);
+
+ const submitHandler = async (e) => {
+ e.preventDefault();
+ if (password !== rePassword) {
+ toast.error("الرمز السري غير متطابق");
+ return;
+ }
+ try {
+ console.log({
+ firstName,
+ middleName,
+ lastName,
+ password,
+ email,
+ phone,
+ gender: gender == "ذكر" ? "male" : "female",
+ });
+ const res = await signup({
+ firstName,
+ middleName,
+ lastName,
+ phone,
+ email,
+ password,
+ gender: gender == "ذكر" ? "male" : "female",
+ }).unwrap();
+ dispatch(setCredentials({ ...res?.data }));
+ navigate("/");
+ } catch (err) {
+ toast.error(err?.data?.message || err.error);
+ console.log(err?.data?.message || err.error);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/client/src/components/signup/signUp.scss b/client/src/components/signup/signUp.scss
new file mode 100644
index 00000000..fff4b7b8
--- /dev/null
+++ b/client/src/components/signup/signUp.scss
@@ -0,0 +1,73 @@
+.signUp {
+ .hero {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: auto;
+ padding: 1.75rem 0.25rem;
+ gap: 1.1875rem;
+ }
+
+ .container {
+ background: var(--Teal-900);
+ border-radius: 1.25rem;
+ display: flex;
+ width: 19rem;
+ padding: 1.5rem 0.4375rem;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 1.0625rem;
+ }
+
+ .card {
+ background-color: var(--grey-800);
+ border-radius: 1.875rem;
+ display: flex;
+ padding: 1.5rem 1rem;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 0.875rem;
+
+ .input-field {
+ display: flex;
+ width: 14.16319rem;
+ padding: 0.125rem 0.75rem;
+ justify-content: flex-end;
+ align-items: center;
+ align-content: center;
+ gap: 0rem 9.25rem;
+ flex-wrap: wrap;
+ input {
+ background: var(--grey-700);
+ margin-block: 0.3rem;
+ border: 1px solid var(--grey-600);
+ border-radius: 0.25rem;
+ width: 12.66319rem;
+ height: 1.75rem;
+ flex-shrink: 0;
+ }
+ }
+
+ .radio-field {
+ display: flex;
+ width: 14.16319rem;
+ padding: 0.5625rem 0.75rem;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: flex-end;
+ flex-wrap: wrap;
+ gap: 0.0625rem;
+ .radio-buttons {
+ display: flex;
+ padding: 0.3125rem 0.5rem;
+ margin: auto;
+ gap: 3.1875rem;
+ .small input {
+ margin: 0.5rem;
+ }
+ }
+ }
+ }
+}
diff --git a/client/src/redux/slices/apiSlice.js b/client/src/redux/slices/apiSlice.js
new file mode 100644
index 00000000..df5ff8e9
--- /dev/null
+++ b/client/src/redux/slices/apiSlice.js
@@ -0,0 +1,9 @@
+import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
+
+const baseQuery = fetchBaseQuery({ baseUrl: "" });
+
+export const apiSlice = createApi({
+ baseQuery,
+ tagTypes: ["Scouts"],
+ endpoints: () => ({}),
+});
diff --git a/client/src/redux/slices/authSlice.js b/client/src/redux/slices/authSlice.js
new file mode 100644
index 00000000..fc61df3e
--- /dev/null
+++ b/client/src/redux/slices/authSlice.js
@@ -0,0 +1,27 @@
+import { createSlice } from "@reduxjs/toolkit";
+
+const initialState = {
+ userInfo: localStorage.getItem("userInfo")
+ ? JSON.parse(localStorage.getItem("userInfo"))
+ : null,
+};
+
+const authSlice = createSlice({
+ name: "auth",
+ initialState,
+ reducers: {
+ setCredentials: (state, action) => {
+ state.userInfo = action.payload;
+ localStorage.setItem("userInfo", JSON.stringify(action.payload));
+ },
+ clearCredentials: (state, action) => {
+ action;
+ state.userInfo = null;
+ localStorage.removeItem("userInfo");
+ },
+ },
+});
+
+export const { setCredentials, clearCredentials } = authSlice.actions;
+
+export default authSlice.reducer;
diff --git a/client/src/redux/slices/userSlice.js b/client/src/redux/slices/userSlice.js
deleted file mode 100644
index f5f3e2af..00000000
--- a/client/src/redux/slices/userSlice.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { createSlice } from "@reduxjs/toolkit"
-
-const initialState = {
- loggedIn: false,
- email: '',
-}
-
-const userSlice = createSlice({
- name: 'user',
- initialState,
- reducers: {
- login: (state, action) => {
- state.loggedIn = true;
- state.email = action.payload.email;
- },
- logout: (state, action) => {
- state.loggedIn = false;
- state.email = '';
- }
- }
-})
-
-export const { login, logout } = userSlice.actions;
-export const selectLoggedIn = (state) => state.user.loggedIn;
-export const selectUser = (state) => state.user;
-
-export default userSlice.reducer
\ No newline at end of file
diff --git a/client/src/redux/slices/usersApiSlice.js b/client/src/redux/slices/usersApiSlice.js
new file mode 100644
index 00000000..e3d8e7f4
--- /dev/null
+++ b/client/src/redux/slices/usersApiSlice.js
@@ -0,0 +1,31 @@
+import { apiSlice } from "./apiSlice";
+
+const USERS_API = "/api/auth";
+
+export const usersApi = apiSlice.injectEndpoints({
+ endpoints: (builder) => ({
+ login: builder.mutation({
+ query: (credentials) => ({
+ url: `${USERS_API}/login`,
+ method: "POST",
+ body: credentials,
+ }),
+ }),
+ logout: builder.mutation({
+ query: () => ({
+ url: `${USERS_API}/logout`,
+ method: "POST",
+ }),
+ }),
+ signup: builder.mutation({
+ query: (data) => ({
+ url: `${USERS_API}/signUp`,
+ method: "POST",
+ body: data,
+ }),
+ }),
+ }),
+});
+
+export const { useLoginMutation, useLogoutMutation, useSignupMutation } =
+ usersApi;
diff --git a/client/src/redux/store.js b/client/src/redux/store.js
index 8a57e88d..2b14e39c 100644
--- a/client/src/redux/store.js
+++ b/client/src/redux/store.js
@@ -1,13 +1,14 @@
-import { configureStore } from "@reduxjs/toolkit"
-import userReducer from "./slices/userSlice"
-import scoutsReducer from "./slices/scoutsSlice"
-
+import { configureStore } from "@reduxjs/toolkit";
+import authReducer from "./slices/authSlice.js";
+import { apiSlice } from "./slices/apiSlice.js";
const store = configureStore({
- reducer: {
- user: userReducer,
- scouts: scoutsReducer,
- },
-})
+ reducer: {
+ auth: authReducer,
+ [apiSlice.reducerPath]: apiSlice.reducer,
+ },
+ middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(apiSlice.middleware),
+ devTools: true,
+});
-export default store;
\ No newline at end of file
+export default store;
diff --git a/client/src/routes.jsx b/client/src/routes.jsx
index c8fa8a8a..4c876f00 100644
--- a/client/src/routes.jsx
+++ b/client/src/routes.jsx
@@ -8,6 +8,11 @@ import {
// Import modules/pages under this line
import LandingPage from "./components/landingpage/LandingPage";
+import SignUp from "./components/signup/signUp";
+import LogIn from "./components/login/logIn";
+
+// Import layouts under this line
+import PublicLayout from "./components/layout/PublicLayout";
// Import Testing Routes here
import TestTypo from "./components/testing/testTypo";
@@ -17,7 +22,11 @@ function Routes() {
return (
- } />
+ }>
+ } />
+ } />
+ } />
+
{/* Testing Routes */}
{/* FIXME: Delete test routes Later */}
diff --git a/client/src/theme.css b/client/src/theme.css
deleted file mode 100644
index e8dd6711..00000000
--- a/client/src/theme.css
+++ /dev/null
@@ -1,52 +0,0 @@
-* {
- margin: 0;
- padding: 0;
-}
-
-:root {
- text-align: center;
- background-color: #111928;
- color: #E2EFEE;
- font-family: "Noto Kufi Arabic";
-}
-
-h1 {
- font-size: 2.98625rem;
- font-weight: 800;
- line-height: 139.429%; /* 4.16369rem */
-}
-
-h2 {
- font-size: 2.48813rem;
- font-weight: 700;
-}
-
-h3 {
- font-size: 2.07375rem;
- font-weight: 700;
-}
-
-h4 {
- font-size: 1.72813rem;
- font-weight: 600;
-}
-
-h5 {
- font-size: 1.44rem;
- font-weight: 600;
-}
-
-h6 {
- font-size: 1.2rem;
- font-weight: 500;
-}
-
-p, label {
- font-size: 1rem;
- font-weight: 500;
-}
-
-small {
- font-size: 0.83313rem;
- font-weight: 400;
-}/*# sourceMappingURL=theme.css.map */
\ No newline at end of file
diff --git a/client/src/theme.scss b/client/src/theme.scss
index f3bda648..9c84607f 100644
--- a/client/src/theme.scss
+++ b/client/src/theme.scss
@@ -91,6 +91,7 @@
--grey-900: #111928;
--bg: #040807;
+ --text-color: #e2efee;
//----------------------------//
// Gradient
diff --git a/client/vite.config.js b/client/vite.config.js
index 861b04b3..830dc010 100644
--- a/client/vite.config.js
+++ b/client/vite.config.js
@@ -1,7 +1,16 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react-swc'
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react-swc";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
-})
+ server: {
+ port: 3000,
+ proxy: {
+ "/api": {
+ target: "http://localhost:5000",
+ changeOrigin: true,
+ },
+ },
+ },
+});
diff --git a/server/.env.example b/server/.env.example
new file mode 100644
index 00000000..d9682b35
--- /dev/null
+++ b/server/.env.example
@@ -0,0 +1,32 @@
+# General Config Variables
+NODE_ENV=
+PORT=
+
+# DB Config Variables
+DB_HOST=
+DB_PORT=
+DB_USER=
+DB_PASS=
+DB_DATABASE=
+
+PORT=5000
+
+# Token Config Variables
+JWT_SECRET=
+JWT_EXPIRES_IN=
+
+########## Example ##########
+# NODE_ENV=development
+# PORT=3000
+
+# DB_HOST=localhost
+# DB_PORT=5432
+# DB_USER=postgres
+# DB_PASS=********
+# DB_DATABASE=scoutsManagementSystem
+# PORT=5000
+
+# To get a random string for JWT_SECRET
+# Run This in terminal => node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
+
+# JWT_EXPIRES_IN='48h'
\ No newline at end of file
diff --git a/server/app.js b/server/app.js
index a493e2a0..b3a17f81 100644
--- a/server/app.js
+++ b/server/app.js
@@ -1,10 +1,11 @@
-const express = require("express");
-const cors = require("cors");
-const db = require("./database/db");
-const app = express();
-const PORT = process.env.PORT || 3000;
-const authRouter = require("./routes/auth.route");
-const alertRouter = require("./routes/alert.route");
+import express from 'express'
+import cors from 'cors'
+import db from './database/db.js'
+import apiRouter from "./routes/api.route.js"
+import { notFound, errorHandler } from './middlewares/error.middleware.js'
+import cookieParser from 'cookie-parser'
+const app = express()
+const PORT = process.env.PORT || 3000
db.connect()
.then(() => {
@@ -14,12 +15,15 @@ db.connect()
if (err) return console.error(err);
});
-app.use(cors());
-app.use(express.json());
+app.use(cors())
+app.use(cookieParser())
+app.use(express.json())
+app.use(express.urlencoded({ extended: true }))
-// Routes
-app.use("/auth", authRouter);
+app.use('/api', apiRouter)
app.use("/alert", alertRouter);
+app.use(notFound)
+app.use(errorHandler)
app.listen(PORT, (err) => {
if (err) return console.error(err);
diff --git a/server/controllers/auth.controller.js b/server/controllers/auth.controller.js
index b577e2c3..569b443d 100644
--- a/server/controllers/auth.controller.js
+++ b/server/controllers/auth.controller.js
@@ -1,125 +1,139 @@
-const bcrypt = require('bcryptjs')
-const jwt = require('jsonwebtoken')
-const db = require('../database/db')
-const { jsonToArray, arrayToJson } = require('../utils/convert')
+import bcrypt from "bcryptjs";
+import db from "../database/db.js";
+import { jsonToArray } from "../utils/convert.js";
+import generateToken from "../utils/generateToken.js";
const authController = {
- signup: async (req, res) => {
- try {
- // get email and password from request body
- const email = req.body['email']
- const password = req.body['password']
-
- // Check if email already exists
- const captain = await db.query(
- `SELECT "email", "password"
+ // @desc Create a new captain
+ // @route POST /api/auth/signup
+ // @access Public
+ signup: async (req, res) => {
+ try {
+ // get email and password from request body
+ const email = req.body["email"];
+ const password = req.body["password"];
+
+ // Check if email already exists
+ const captain = await db.query(
+ `SELECT "email", "password"
FROM "Captain"
WHERE "email" = $1;`,
- [email]
- )
- if (captain.rows.length) {
- return res.status(400).json({ error: 'Email is taken!!' })
- }
-
- // Hash the password
- const hashedPassword = await bcrypt.hash(password, 10)
-
- // Create a new Captain
- req.body = { ...req.body, password: hashedPassword }
- const params = jsonToArray(req.body)
- const result = await db.query(
- `INSERT INTO "Captain"("firstName", "middleName", "lastName", "email", "password", "phoneNumber", "gender", "type")
+ [email]
+ );
+ if (captain.rows.length) {
+ return res.status(400).json({ error: "Email is taken!!" });
+ }
+
+ // Hash the password
+ const hashedPassword = await bcrypt.hash(password, 10);
+
+ // Create a new Captain
+ req.body = { ...req.body, password: hashedPassword };
+ const params = jsonToArray(req.body);
+ const result = await db.query(
+ `INSERT INTO "Captain"("firstName", "middleName", "lastName", "phoneNumber", "email", "password", "gender", "type")
VALUES($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *;`,
- params.concat(['regular'])
- )
- const newCaptain = result.rows[0]
-
- // Generate a JWT token containing the captain's id
- // Bearer token is the token that we will send to the client
- const token = jwt.sign(
- { id: newCaptain.captainId }, // Payload
- process.env.JWT_SECRET,
- {
- expiresIn: process.env.JWT_EXPIRES_IN,
- }
- )
-
- // Send the response
- res.status(201).json({
- message: 'Captain created successfully',
- newCaptain,
- token,
- })
- } catch (error) {
- console.log(error)
- res.status(500).json({
- error: 'An error occurred while creating a new captain!!',
- })
- }
- },
-
- login: async (req, res) => {
- try {
- // Deconstruct the request body
- const { email, password } = req.body
-
- // Check if email already exists
- const result = await db.query(
- `SELECT "email", "password"
+ params.concat(["regular"])
+ );
+ const newCaptain = result.rows[0];
+
+ // Generate a JWT token
+ generateToken(res, newCaptain.captainId);
+
+ // Send the response
+ res.status(201).json({
+ message: "Captain created successfully",
+ data: newCaptain,
+ });
+ } catch (error) {
+ console.log(error);
+ res.status(500).json({
+ error: "An error occurred while creating a new captain!!",
+ });
+ }
+ },
+
+ // @desc Login a captain
+ // @route POST /api/auth/login
+ // @access Public
+ login: async (req, res) => {
+ try {
+ // Deconstruct the request body
+ const { email, password } = req.body;
+
+ // Check if email already exists
+ const result = await db.query(
+ `SELECT *
FROM "Captain"
WHERE "email" = $1;`,
- [email]
- )
- if (!result.rows.length) {
- return res.status(400).json({
- error: 'Invalid email',
- })
- }
-
- // Get Captain's data
- const captain = result.rows[0]
-
- // Check if the password is correct
- const isCorrect = await bcrypt.compare(password, captain.password)
- if (!isCorrect) {
- return res.status(400).json({
- error: 'Invalid password',
- })
- }
-
- // Generate a JWT token containing the captain's id
- // Bearer token is the token that we will send to the client
- const token = jwt.sign(
- { id: captain.captainId }, // Payload
- process.env.JWT_SECRET,
- {
- expiresIn: process.env.JWT_EXPIRES_IN,
- }
- )
-
- // Send the response
- res.status(200).json({
- message: 'Logged in successfully',
- token,
- })
- } catch (error) {
- console.log(error)
- res.status(500).json({
- error: 'An error occurred while logging you in',
- })
- }
- },
-
- // This controller is responsible for fetching data of the logged-in captain
- me: (req, res) => {
- try {
- res.status(200).json({ user: req.captain })
- } catch (error) {
- console.log(error)
- res.status(500).json({
- error: 'An error occurred while fetching data.',
- })
- }
- },
-}
-module.exports = authController
+ [email]
+ );
+ if (!result.rows.length) {
+ return res.status(400).json({
+ error: "Invalid email",
+ });
+ }
+
+ // Get Captain's data
+ const captain = result.rows[0];
+
+ // Check if the password is correct
+ const isCorrect = await bcrypt.compare(password, captain.password);
+ if (!isCorrect) {
+ return res.status(400).json({
+ error: "Invalid password",
+ });
+ }
+
+ // Generate a JWT token
+ generateToken(res, captain.captainId);
+
+ // Send the response
+ res.status(200).json({
+ message: "Logged in successfully",
+ data: captain,
+ });
+ } catch (error) {
+ console.log(error);
+ res.status(500).json({
+ error: "An error occurred while logging you in",
+ });
+ }
+ },
+
+ // @desc Logout a captain
+ // @route GET /api/auth/logout
+ // @access Private
+ logout: async (req, res) => {
+ try {
+ // Clear the cookie
+ res.clearCookie("token");
+
+ // Send the response
+ res.status(200).json({
+ message: "Logged out successfully",
+ });
+ } catch (error) {
+ console.log(error);
+ res.status(500).json({
+ error: "An error occurred while logging out",
+ });
+ }
+ },
+
+ // @desc Auth logged-in captain
+ // @route GET /api/auth/me
+ // @access Private
+ me: (req, res) => {
+ try {
+ res.status(200).json({ user: req.captain });
+ } catch (error) {
+ console.log(error);
+ res.status(500).json({
+ error: "An error occurred while fetching data.",
+ });
+ }
+ },
+};
+
+export default authController;
diff --git a/server/database/db.js b/server/database/db.js
index c97b4b00..453c1e6c 100644
--- a/server/database/db.js
+++ b/server/database/db.js
@@ -1,5 +1,5 @@
-const pg = require("pg");
-const dotenv = require("dotenv");
+import pg from 'pg'
+import dotenv from 'dotenv'
dotenv.config();
const db = new pg.Pool({
@@ -10,4 +10,5 @@ const db = new pg.Pool({
database: process.env.DB_DATABASE,
});
-module.exports = db;
+
+export default db
diff --git a/server/database/fillDatabase.sql b/server/database/fillDatabase.sql
new file mode 100644
index 00000000..3e735bb3
--- /dev/null
+++ b/server/database/fillDatabase.sql
@@ -0,0 +1,23 @@
+-- ADD CAPTAINS
+INSERT INTO
+ "Captain"(
+ "firstName",
+ "middleName",
+ "lastName",
+ "phoneNumber",
+ "email",
+ "password",
+ "gender",
+ "type"
+ )
+VALUES
+ (
+ "أمير",
+ "أنور",
+ "بخيت",
+ "01221461992",
+ "amir.kedis@gmail.com",
+ "$2a$10$82orQ3yruIoakCWUg/29KuXBwJlZiezJzxUW.8Ek.Jvc/MPLagDYS",
+ "male",
+ "regular"
+ ) RETURNING *;
\ No newline at end of file
diff --git a/server/middlewares/auth.middleware.js b/server/middlewares/auth.middleware.js
index ac5cbb34..b34b3db6 100644
--- a/server/middlewares/auth.middleware.js
+++ b/server/middlewares/auth.middleware.js
@@ -1,21 +1,14 @@
-const jwt = require('jsonwebtoken')
-const db = require('../database/db')
+import jwt from 'jsonwebtoken'
+import db from '../database/db.js'
const authMiddleware = async (req, res, next) => {
- // Get authorization header and check if it exists
- const auth = req.headers.authorization
- if (!auth) {
- return res.status(401).json({ error: 'No token provided' })
- }
+ // Get token from cookie
+ const token = req.cookies.token
- // Check if the authorization header is "Bearer "
- if (!auth.startsWith('Bearer') || auth.split(' ').length !== 2) {
- return res.status(401).json({ error: 'Invalid token, not Bearer' })
+ if (!token) {
+ return res.status(401).json({ error: 'No token provided' })
}
- // Get token from the authorization header
- const token = auth.split(' ')[1]
-
try {
// Verify token and get captain's id
const id = jwt.verify(token, process.env.JWT_SECRET).id
@@ -44,4 +37,4 @@ const authMiddleware = async (req, res, next) => {
}
}
-module.exports = authMiddleware
+export default authMiddleware
diff --git a/server/middlewares/error.middleware.js b/server/middlewares/error.middleware.js
new file mode 100644
index 00000000..aff17de3
--- /dev/null
+++ b/server/middlewares/error.middleware.js
@@ -0,0 +1,16 @@
+const notFound = (req, res, next) => {
+ const error = new Error(`Not found - ${req.originalUrl}`)
+ res.status(404)
+ next(error)
+}
+
+const errorHandler = (error, req, res, next) => {
+ const statusCode = res.statusCode === 200 ? 500 : res.statusCode
+
+ res.status(statusCode).json({
+ message: error.message,
+ stack: process.env.NODE_ENV === 'production' ? null : error.stack,
+ })
+}
+
+export { notFound, errorHandler }
\ No newline at end of file
diff --git a/server/package-lock.json b/server/package-lock.json
index a6128cfd..87b5f086 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -10,10 +10,12 @@
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
+ "cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
+ "ms": "^2.1.3",
"pg": "^8.11.3"
},
"devDependencies": {
@@ -215,6 +217,26 @@
"node": ">= 0.6"
}
},
+ "node_modules/cookie-parser": {
+ "version": "1.4.6",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
+ "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
+ "dependencies": {
+ "cookie": "0.4.1",
+ "cookie-signature": "1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/cookie-parser/node_modules/cookie": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@@ -240,6 +262,11 @@
"ms": "2.0.0"
}
},
+ "node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
"node_modules/define-data-property": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
@@ -621,11 +648,6 @@
"npm": ">=6"
}
},
- "node_modules/jsonwebtoken/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
- },
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
@@ -755,9 +777,9 @@
}
},
"node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/negotiator": {
"version": "0.6.3",
@@ -1144,11 +1166,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/send/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
- },
"node_modules/serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
diff --git a/server/package.json b/server/package.json
index 9cac5295..9fc1dc6a 100644
--- a/server/package.json
+++ b/server/package.json
@@ -2,19 +2,22 @@
"name": "server",
"version": "1.0.0",
"description": "",
+ "type": "module",
"main": "app.js",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1",
- "start": "nodemon app.js"
+ "start": "node app.js",
+ "server": "nodemon app.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
+ "cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
+ "ms": "^2.1.3",
"pg": "^8.11.3"
},
"devDependencies": {
diff --git a/server/routes/api.route.js b/server/routes/api.route.js
new file mode 100644
index 00000000..da9b32f2
--- /dev/null
+++ b/server/routes/api.route.js
@@ -0,0 +1,7 @@
+import { Router } from 'express'
+import authRouter from './auth.route.js'
+const apiRouter = Router()
+
+apiRouter.use('/auth', authRouter)
+
+export default apiRouter
diff --git a/server/routes/auth.route.js b/server/routes/auth.route.js
index 880e7482..93f1f7ce 100644
--- a/server/routes/auth.route.js
+++ b/server/routes/auth.route.js
@@ -1,9 +1,11 @@
-const authRouter = require("express").Router();
-const authController = require("../controllers/auth.controller");
-const authMiddleware = require("../middlewares/auth.middleware");
+import { Router } from 'express'
+import authController from '../controllers/auth.controller.js'
+import authMiddleware from '../middlewares/auth.middleware.js'
+const authRouter = Router()
-authRouter.post("/signUp", authController.signup);
-authRouter.post("/logIn", authController.login);
-authRouter.get("/me", authMiddleware, authController.me);
+authRouter.post('/signUp', authController.signup)
+authRouter.post('/logIn', authController.login)
+authRouter.get('/logOut', authMiddleware, authController.logout)
+authRouter.get('/me', authMiddleware, authController.me)
-module.exports = authRouter;
\ No newline at end of file
+export default authRouter
diff --git a/server/utils/convert.js b/server/utils/convert.js
index 69ac8ce7..46238426 100644
--- a/server/utils/convert.js
+++ b/server/utils/convert.js
@@ -14,7 +14,4 @@ const arrayToJson = (arr) => {
return json
}
-module.exports = {
- jsonToArray,
- arrayToJson,
-}
+export { jsonToArray, arrayToJson }
diff --git a/server/utils/generateToken.js b/server/utils/generateToken.js
new file mode 100644
index 00000000..a2b2a6c8
--- /dev/null
+++ b/server/utils/generateToken.js
@@ -0,0 +1,26 @@
+import jwt from 'jsonwebtoken'
+import ms from 'ms'
+
+const generateToken = (res, id) => {
+ // Generate a JWT token containing id
+ // Bearer token is the token that we will be send to the client
+ const token = jwt.sign(
+ { id: id }, // Payload
+ process.env.JWT_SECRET,
+ {
+ expiresIn: process.env.JWT_EXPIRES_IN,
+ }
+ )
+
+ const maxAge = ms(process.env.JWT_EXPIRES_IN)
+
+ // Save token in cookie
+ res.cookie('token', token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV !== 'development',
+ sameSite: 'strict',
+ maxAge: maxAge,
+ })
+}
+
+export default generateToken