aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.js4
-rw-r--r--src/App.scss26
-rw-r--r--src/classes/invoice.js1
-rw-r--r--src/classes/user.js71
-rw-r--r--src/components/editors/invoice-headers-editor.js198
-rw-r--r--src/components/editors/scss/_editor.scss16
-rw-r--r--src/components/editors/scss/invoice-headers.scss6
-rw-r--r--src/index.js59
-rw-r--r--src/views/invoice/new.js14
-rw-r--r--src/views/login/login.js100
-rw-r--r--src/views/login/register.js104
-rw-r--r--src/views/login/scss/login.scss67
-rw-r--r--src/views/manage/scss/management-page.scss8
13 files changed, 588 insertions, 86 deletions
diff --git a/src/App.js b/src/App.js
index a7a2e5c..9fd5081 100644
--- a/src/App.js
+++ b/src/App.js
@@ -19,6 +19,8 @@ import { BrowserRouter, Route, Routes } from "react-router-dom";
import './App.scss';
import Navbar from './components/navbar/navbar';
import HomePage from './views/homepage';
+import RegisterPage from './views/login/register';
+import LoginPage from './views/login/login';
import NewInvoicePage from './views/invoice/new';
import ManagementPage from './views/manage/manage';
import ManageItemsPage from './views/manage/items';
@@ -33,6 +35,8 @@ const App = () => {
<main>
<Routes>
<Route exact path="/" element={<HomePage/>}/>
+ <Route exact path="/login" element={<LoginPage/>}/>
+ <Route exact path="/register" element={<RegisterPage/>}/>
<Route exact path="/invoice/new" element={<NewInvoicePage/>}/>
<Route exact path="/manage/items" element={<ManageItemsPage/>}/>
<Route exact path="/manage/clients" element={<ManageClientsPage/>}/>
diff --git a/src/App.scss b/src/App.scss
index 23ee270..1b37ffe 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -60,3 +60,29 @@ $selectionColor: rgba($primaryAccentColor, 0.9)
color: $darkgray;
background: $selectionColor;
}
+
+.dropdown-icon {
+ transition: transform 0.4s;
+ &.open {
+ transform: rotate(-180deg);
+ }
+}
+
+.dropdown-div {
+ animation: dropdown ease 0.15s;
+}
+
+@keyframes dropdown {
+ 0% {
+ opacity: 0;
+ transform: translateY(-100%)
+ }
+ 50% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 100;
+ transform: translateY(0px)
+ }
+}
+
diff --git a/src/classes/invoice.js b/src/classes/invoice.js
index 066cbc3..610b7d8 100644
--- a/src/classes/invoice.js
+++ b/src/classes/invoice.js
@@ -15,7 +15,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { InvoiceItem } from "./item";
import { Client, Address } from "./client";
import axios from "axios";
diff --git a/src/classes/user.js b/src/classes/user.js
new file mode 100644
index 0000000..858d8d6
--- /dev/null
+++ b/src/classes/user.js
@@ -0,0 +1,71 @@
+/* OpenBills-web - Web based libre billing software
+ * Copyright (C) 2022 Vidhu Kant Sharma <vidhukant@vidhukant.xyz>
+
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import axios from "axios";
+
+export class User {
+ constructor() {
+ this.Id = null;
+ this.UserName = "";
+ this.Email = "";
+ this.Password = "";
+ }
+}
+
+export const validateUsername = username => {
+ if (username.length < 2) return false;
+ // username can't have spaces
+ if (username.includes(" ")) return false;
+ return true
+}
+
+export const validatePassword = password => {
+ if (password.length < 12) return false;
+ // TODO: add other validation
+
+ return true;
+}
+
+export const validateEmail = email => String(email)
+ .toLowerCase()
+ .match(
+ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+ ) ? true : false;
+
+export const login = (data, ok, fail) => {
+ axios.post("/auth/login", data)
+ .then(res => ok(res))
+ .catch(err => fail(err));
+}
+
+export const saveUser = (user, ok, fail) => {
+ axios.post("/user/new", user)
+ .then(res => ok(res))
+ .catch(err => fail(err));
+}
+
+export const deleteUser = (id, ok, fail) => {
+ axios.delete(`/user/${id}`)
+ .then(res => ok(res))
+ .catch(err => fail(err));
+}
+
+export const editUser = (user, ok, fail) => {
+ axios.put(`/user/${user.Id}`, user)
+ .then(res => ok(res))
+ .catch(err => fail(err));
+}
diff --git a/src/components/editors/invoice-headers-editor.js b/src/components/editors/invoice-headers-editor.js
index d099e59..dcbf9ca 100644
--- a/src/components/editors/invoice-headers-editor.js
+++ b/src/components/editors/invoice-headers-editor.js
@@ -18,8 +18,14 @@
import './scss/invoice-headers.scss';
import { useState, useEffect } from 'react';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
+
+const InvoiceHeadersEditor = ({roundOff, setRoundOff, transport, setTransport, invoiceNumber, setInvoiceNumber, date, setDate}) => {
+ // show transport details menu
+ const [showTPMenu, setShowTPMenu] = useState(false);
+ const [showAdditionalMenu, setShowAdditionalMenu] = useState(false);
-const InvoiceHeadersEditor = ({roundOff, setRoundOff, transport, setTransport}) => {
const handleInput = e => {
const { name, value } = e.target;
@@ -43,79 +49,123 @@ const InvoiceHeadersEditor = ({roundOff, setRoundOff, transport, setTransport})
return (
<div className={"invoice-headers-editor"}>
<h1>Invoice Options:</h1>
- <div>
- <label className={"checkbox-label"}>
- <input
- type="checkbox"
- onChange={() => setRoundOff(prev => !prev)}
- checked={roundOff}/>
- Round off the Total
- </label>
-
- <label>
- Apply Discount On All Items:
- <input
- className={"small"}
- type="number"
- min="0"
- max="100"
- step="0.1" />
- </label>
-
- <p><strong>Transport Details</strong></p>
-
- <label>
- Vehicle Number:
- <input
- name="VehicleNum"
- value={transport.VehicleNum}
- onChange={handleInput}
- type="text"/>
- </label>
-
- <label>
- Transport Method:
- <input
- name="TransportMethod"
- value={transport.TransportMethod}
- onChange={handleInput}
- type="text"/>
- </label>
-
- <label>
- Transporter Name:
- <input
- name="Transporter.Name"
- value={transport.Transporter.Name}
- onChange={handleInput}
- type="text"/>
- </label>
-
- <label>
- Transporter GSTIN:
- <input
- name="Transporter.GSTIN"
- value={transport.Transporter.GSTIN}
- onChange={handleInput}
- type="text"/>
- </label>
-
- <label>
- Transporter ID:
- <input
- name="Transporter.TransporterId"
- value={transport.Transporter.TransporterId}
- onChange={handleInput}
- type="text"/>
- </label>
-
- <label>
- Delivery Note:
- <textarea
- name="Note"
- value={transport.Note}
- onChange={handleInput} />
- </label>
+ <div className={"wrapper"}>
+ <div>
+ <label className={"checkbox-label"}>
+ <input
+ type="checkbox"
+ onChange={() => setRoundOff(prev => !prev)}
+ checked={roundOff}/>
+ Round off the Total
+ </label>
+
+ <label>
+ Apply Discount On All Items:
+ <input
+ className={"small"}
+ type="number"
+ min="0"
+ max="100"
+ step="0.1" />
+ </label>
+
+ <p onClick={() => setShowTPMenu(i => !i)}>
+ <strong>
+ Transport Details <FontAwesomeIcon
+ icon={faChevronDown}
+ className={`dropdown-icon ${showTPMenu ? "open" : "closed"}`} />
+ </strong>
+ </p>
+ <hr/>
+ {showTPMenu &&
+ <div className={"dropdown-div"}>
+ <label>
+ Vehicle Number:
+ <input
+ name="VehicleNum"
+ value={transport.VehicleNum}
+ onChange={handleInput}
+ type="text"/>
+ </label>
+
+ <label>
+ Transport Method:
+ <input
+ name="TransportMethod"
+ value={transport.TransportMethod}
+ onChange={handleInput}
+ type="text"/>
+ </label>
+
+ <label>
+ Transporter Name:
+ <input
+ name="Transporter.Name"
+ value={transport.Transporter.Name}
+ onChange={handleInput}
+ type="text"/>
+ </label>
+
+ <label>
+ Transporter GSTIN:
+ <input
+ name="Transporter.GSTIN"
+ value={transport.Transporter.GSTIN}
+ onChange={handleInput}
+ type="text"/>
+ </label>
+
+ <label>
+ Transporter ID:
+ <input
+ name="Transporter.TransporterId"
+ value={transport.Transporter.TransporterId}
+ onChange={handleInput}
+ type="text"/>
+ </label>
+
+ <label>
+ Delivery Note:
+ <textarea
+ name="Note"
+ value={transport.Note}
+ onChange={handleInput} />
+ </label>
+ </div>
+ }
+ <p onClick={() => setShowAdditionalMenu(i => !i)}>
+ <strong>
+ Additional Details <FontAwesomeIcon
+ icon={faChevronDown}
+ className={`dropdown-icon ${showAdditionalMenu ? "open" : "closed"}`} />
+ </strong>
+ </p>
+ <hr/>
+ {showAdditionalMenu &&
+ <div className={"dropdown-div"}>
+ <label>
+ Placeholder:
+ <input
+ //name="VehicleNum"
+ //value={transport.VehicleNum}
+ //onChange={handleInput}
+ type="text"/>
+ </label>
+ </div>
+ }
+ </div>
+ <div>
+ <label>
+ Invoice Number:
+ <input
+ type="text"
+ value={invoiceNumber}
+ onChange={(e) => setInvoiceNumber(e.target.value)} />
+ </label>
+ <label>
+ Invoice Date:
+ </label>
+ </div>
</div>
</div>
)
diff --git a/src/components/editors/scss/_editor.scss b/src/components/editors/scss/_editor.scss
index 1ca7067..6cd7c43 100644
--- a/src/components/editors/scss/_editor.scss
+++ b/src/components/editors/scss/_editor.scss
@@ -17,6 +17,22 @@
@import "colors";
+@mixin button {
+ button, input[type=submit] {
+ padding: 0.2rem 0;
+ width: 4rem;
+ background-color: $inputBackgroundColor;
+ border: 1px solid $primaryAccentColor;
+ color: $fgColor;
+ border-radius: 4px;
+ transition: background-color 0.4s, color 0.4s;
+ }
+ button:hover, input[type=submit]:hover {
+ background-color: $primaryAccentColor;
+ color: $fgColorAlt;
+ }
+}
+
@mixin label {
label {
display: flex;
diff --git a/src/components/editors/scss/invoice-headers.scss b/src/components/editors/scss/invoice-headers.scss
index ade428b..56055e7 100644
--- a/src/components/editors/scss/invoice-headers.scss
+++ b/src/components/editors/scss/invoice-headers.scss
@@ -42,6 +42,12 @@
}
}
+ .wrapper {
+ display: flex;
+ justify-content: space-between;
+ width: 44rem;
+ }
+
// hide up/down arrows from number input
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
diff --git a/src/index.js b/src/index.js
index 593edf1..2a7a75d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,6 +1,65 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
+import axios from 'axios';
+
+// For GET requests
+axios.interceptors.request.use(
+ config => {
+ const token = localStorage.getItem("accessToken");
+ if (token) config.headers.Authorization = token;
+ return config;
+ },
+ err => new Promise((resolve) => {
+ if (err.response && err.response.status === 401) {
+ err.config._retry = true;
+
+ const response = fetch("/auth/refresh", {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ })
+ .then((res) => res.json())
+ .then((res) => {
+ localStorage.setItem("accessToken", res.accessToken);
+ return axios(err.config);
+ })
+ resolve(response);
+ } else {
+ return Promise.reject(err);
+ }
+ })
+);
+
+// For POST requests
+axios.interceptors.response.use(
+ config => {
+ const token = localStorage.getItem("accessToken");
+ if (token) config.headers.Authorization = token;
+ return config;
+ },
+ err => new Promise((resolve) => {
+ if (err.response && err.response.status === 401 && err.config.url !== "/auth/login") {
+ err.config._retry = true;
+
+ const response = fetch("/auth/refresh", {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ })
+ .then((res) => res.json())
+ .then((res) => {
+ localStorage.setItem("accessToken", res.accessToken);
+ return axios(err.config);
+ })
+ resolve(response);
+ } else {
+ return Promise.reject(err);
+ }
+ })
+);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
diff --git a/src/views/invoice/new.js b/src/views/invoice/new.js
index 2d88e40..b1d601f 100644
--- a/src/views/invoice/new.js
+++ b/src/views/invoice/new.js
@@ -36,6 +36,8 @@ const NewInvoicePage = () => {
const [roundOffTotal, setRoundOffTotal] = useState(true); //TODO: load from config
//const [isInterstate, setIsInterstate] = useState(false);
const [transport, setTransport] = useState(new Transport());
+ const [invoiceNumber, setInvoiceNumber] = useState("0"); // TODO: auto increment
+ const [invoiceDate, setInvoiceDate] = useState(new Date());
const isInterstate = false; // temporary
const [sum, setSum] = useState({
GST: currency(0),
@@ -47,8 +49,8 @@ const NewInvoicePage = () => {
const submitInvoice = () => {
const invoice = new Invoice();
- invoice.InvoiceNumber = 69; // TODO: set accordingly
- invoice.CreatedAt = new Date();
+ invoice.InvoiceNumber = invoiceNumber;
+ invoice.CreatedAt = invoiceDate;
invoice.TotalAmount = sum.Amount;
const recipient = new Client();
@@ -67,7 +69,7 @@ const NewInvoicePage = () => {
invoice.Note = ""; // TODO: set accordingly
invoice.Draft = false; // TODO: set accordingly
- saveInvoice(invoice, handleSuccess, handleFail)
+ saveInvoice(invoice, handleSuccess, handleFail);
}
const handleSuccess = () => {
@@ -102,7 +104,11 @@ const NewInvoicePage = () => {
roundOff={roundOffTotal}
setRoundOff={setRoundOffTotal}
transport={transport}
- setTransport={setTransport} />
+ setTransport={setTransport}
+ invoiceNumber={invoiceNumber}
+ setInvoiceNumber={setInvoiceNumber}
+ date={invoiceDate}
+ setDate={setInvoiceDate} />
<div>
<InvoiceSummary
sum={sum}
diff --git a/src/views/login/login.js b/src/views/login/login.js
new file mode 100644
index 0000000..0283dc7
--- /dev/null
+++ b/src/views/login/login.js
@@ -0,0 +1,100 @@
+/* OpenBills-web - Web based libre billing software
+ * Copyright (C) 2022 Vidhu Kant Sharma <vidhukant@vidhukant.xyz>
+
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import './scss/login.scss';
+import { User, login } from '../../classes/user';
+
+import { Link, useNavigate } from 'react-router-dom';
+import { useState } from 'react';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faEye } from '@fortawesome/free-solid-svg-icons'
+
+const LoginPage = () => {
+ const [user, setUser] = useState(new User());
+ const [showPassword, setShowPassword] = useState(false);
+ const navigate = useNavigate();
+
+ const validate = () => {
+ if (user.UserName.trim() === "") return false;
+ if (user.Password.length < 8) return false;
+ return true;
+ }
+
+ const handleInput = ({target: {name, value}}) =>
+ setUser(prev => ({...prev, [name]: value}));
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ login(user, handleSuccess, handleError);
+ }
+
+ const handleSuccess = (res) => {
+ localStorage.setItem("accessToken", res.data.accessToken)
+ navigate("/")
+ }
+
+ const handleError = (err) => {
+ console.log(err)
+ alert("fail")
+ }
+
+ return (
+ <div className={"login-page-wrapper"}>
+ <div className={"login-page"}>
+ <h1>Welcome To OpenBills!</h1>
+ <p>You are not logged in.</p>
+ <form onSubmit={handleSubmit}>
+ <label>
+ Username:
+ <input
+ className={"wider"}
+ name="UserName"
+ type="text"
+ value={user.UserName}
+ onChange={handleInput}/>
+ </label>
+ <label>
+ Password:
+ <span className={"input-with-icon"}>
+ <input
+ name="Password"
+ type={showPassword ? "text" : "password"}
+ value={user.Password}
+ onChange={handleInput} />
+ <FontAwesomeIcon
+ icon={faEye}
+ className={`icon ${showPassword ? "active" : ""}`}
+ onClick={(e) => {
+ e.preventDefault();
+ setShowPassword(i => !i);
+ }}/>
+ </span>
+ </label>
+ <hr/>
+ <div className={"buttons"}>
+ <Link to="/register">
+ <button>Create Account</button>
+ </Link>
+ <input type="submit" value="Log In" disabled={!validate()}/>
+ </div>
+ </form>
+ </div>
+ </div>
+ );
+}
+
+export default LoginPage;
diff --git a/src/views/login/register.js b/src/views/login/register.js
new file mode 100644
index 0000000..c747f59
--- /dev/null
+++ b/src/views/login/register.js
@@ -0,0 +1,104 @@
+/* OpenBills-web - Web based libre billing software
+ * Copyright (C) 2022 Vidhu Kant Sharma <vidhukant@vidhukant.xyz>
+
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import './scss/login.scss';
+import { User, validateEmail, validateUsername, validatePassword, saveUser } from '../../classes/user';
+
+import { Link } from 'react-router-dom';
+import { useState } from 'react';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faEye } from '@fortawesome/free-solid-svg-icons'
+
+const RegisterPage = () => {
+ const [user, setUser] = useState(new User());
+ const [showPassword, setShowPassword] = useState(false);
+
+ const validate = () =>
+ validateUsername(user.UserName.trim()) &&
+ validateEmail(user.Email) &&
+ validatePassword(user.Password);
+
+ const handleInput = ({target: {name, value}}) =>
+ setUser(prev => ({...prev, [name]: value}));
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ saveUser(user, handleSuccess, handleError);
+ }
+
+ const handleSuccess = () => {
+ alert("yay")
+ }
+
+ const handleError = () => {
+ alert("fail")
+ }
+
+ return (
+ <div className={"register-page-wrapper"}>
+ <div className={"register-page"}>
+ <h1>Sign Up To OpenBills</h1>
+ <form onSubmit={handleSubmit}>
+ <label>
+ Username:
+ <input
+ className={"wider"}
+ name="UserName"
+ type="text"
+ value={user.UserName}
+ onChange={handleInput}/>
+ </label>
+ <label>
+ E-mail:
+ <input
+ className={"wider"}
+ name="Email"
+ type="text"
+ value={user.Email}
+ onChange={handleInput}/>
+ </label>
+ <label>
+ Password:
+ <span className={"input-with-icon"}>
+ <input
+ name="Password"
+ type={showPassword ? "text" : "password"}
+ value={user.Password}
+ onChange={handleInput} />
+ <FontAwesomeIcon
+ icon={faEye}
+ className={`icon ${showPassword ? "active" : ""}`}
+ onClick={(e) => {
+ e.preventDefault();
+ setShowPassword(i => !i);
+ }}/>
+ </span>
+ </label>
+ <hr/>
+ <div className={"buttons"}>
+ <Link to="/login">
+ <button>Log In Instead</button>
+ </Link>
+ <input type="submit" value="Sign Up" disabled={!validate()}/>
+ </div>
+ </form>
+ </div>
+ </div>
+ );
+}
+
+export default RegisterPage;
diff --git a/src/views/login/scss/login.scss b/src/views/login/scss/login.scss
new file mode 100644
index 0000000..1b2be62
--- /dev/null
+++ b/src/views/login/scss/login.scss
@@ -0,0 +1,67 @@
+@import "../../../colors";
+@import "../../../components/editors/scss/editor";
+
+.login-page-wrapper, .register-page-wrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: calc(100vh - 7rem);
+}
+
+.login-page, .register-page {
+ @include label;
+ @include button;
+ width: 95%;
+ max-width: 25rem;
+ margin-top: -6rem;
+ h1, p {
+ margin: 0.5rem 0;
+ text-align: center;
+ }
+
+ label {
+ margin: auto;
+ width: 98%;
+ max-width: none;
+ }
+
+ hr {
+ margin-top: 1rem;
+ }
+
+ .input-with-icon {
+ width: 100%;
+ max-width: 15rem;
+ input {
+ margin-right: 0.5rem;
+ width: 87%;
+ }
+ .icon {
+ cursor: pointer;
+ transition: color 0.2s;
+ }
+ .icon.active {color: $primaryAccentColor;}
+ }
+
+ input.wider {
+ max-width: 15rem;
+ }
+
+ .buttons {
+ margin: 1rem 0;
+ display: flex;
+ justify-content: center;
+ button {width: 10rem;}
+ button, input[type=submit] {
+ margin: 0 1rem;
+ }
+ }
+
+ input[type=submit]:disabled {
+ border-color: $warningColor;
+ }
+ input[type=submit]:disabled:hover {
+ background-color: $warningColor;
+ }
+}
diff --git a/src/views/manage/scss/management-page.scss b/src/views/manage/scss/management-page.scss
index f61b62d..85fbe7b 100644
--- a/src/views/manage/scss/management-page.scss
+++ b/src/views/manage/scss/management-page.scss
@@ -15,13 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-@import "../../../styles";
-
-@include floating-window;
-
-hr {
- margin: 0.8rem auto 1rem auto;
-}
+@import "../../../colors";
.manage-links {
max-width: 40rem;