-
@@ -51,7 +53,11 @@ function NewEntry() {
rules={[
{
required: true,
- message: 'Please insert a URL'
+ message: 'Please insert a url.'
+ },
+ {
+ type: 'url',
+ message: 'This field must be a valid url.'
}
]}>
@@ -68,4 +74,4 @@ function NewEntry() {
);
}
-export default NewEntry;
+export default NewLink;
diff --git a/components/Admin/LinksTable/index.js b/components/Admin/LinksTable/index.js
index 6e4a19d..3b8a597 100644
--- a/components/Admin/LinksTable/index.js
+++ b/components/Admin/LinksTable/index.js
@@ -5,7 +5,7 @@ import { Table, Checkbox, notification } from 'antd';
import { Twemoji } from 'react-emoji-render';
import { sortableContainer, sortableElement, sortableHandle } from 'react-sortable-hoc';
import Actions from './Actions';
-import NewEntry from './NewEntry';
+import NewLink from './NewLink';
import API from '../../../utils/api';
import styles from './style.module.css';
@@ -105,7 +105,7 @@ function LinksTable() {
})
.catch((error) => {
notification['error']({
- message: `${error.response.statusText}`,
+ message: `${error.statusText}`,
description: error.message
});
});
@@ -119,7 +119,6 @@ function LinksTable() {
columns={columns}
dataSource={links}
bordered
- // pagination={{ position: ['bottomCenter'] }}
pagination={false}
components={{
body: {
@@ -127,7 +126,7 @@ function LinksTable() {
row: DraggableBodyRow
}
}}
- footer={() => }
+ footer={() => }
/>
);
}
diff --git a/components/Admin/Navbar/index.js b/components/Admin/Navbar/index.js
index 67adcbc..dfb389c 100644
--- a/components/Admin/Navbar/index.js
+++ b/components/Admin/Navbar/index.js
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
-import Link from 'next/link';
-import { Avatar, Dropdown, Menu } from 'antd';
-import { LinkOutlined, UserOutlined } from '@ant-design/icons';
+import LinkTo from '../../utils/LinkTo';
+import { Avatar, Menu, Typography, Space } from 'antd';
+import { LinkOutlined, FormOutlined, UserOutlined } from '@ant-design/icons';
import API from '../../../utils/api';
@@ -11,6 +11,10 @@ export const navbar = {
links: {
icon: ,
title: 'Links'
+ },
+ forms: {
+ icon: ,
+ title: 'Forms'
}
};
@@ -25,26 +29,21 @@ function Navbar({ selected }) {
+ title={
+
+ } />
+ {user.name}
+
}>
-
-
+
+ logout
+
+
);
}
diff --git a/components/Admin/Navbar/style.module.css b/components/Admin/Navbar/style.module.css
index 646acfa..8586455 100644
--- a/components/Admin/Navbar/style.module.css
+++ b/components/Admin/Navbar/style.module.css
@@ -1,5 +1,3 @@
.avatar {
float: right;
- margin-right: 10px;
- cursor: pointer;
}
diff --git a/components/EditableTable/EditableCell.js b/components/EditableTable/EditableCell.js
new file mode 100644
index 0000000..17f56d4
--- /dev/null
+++ b/components/EditableTable/EditableCell.js
@@ -0,0 +1,37 @@
+import { Input, InputNumber, Form } from 'antd';
+
+const EditableCell = ({
+ editing,
+ dataIndex,
+ required = true,
+ title,
+ inputType = 'text',
+ children,
+ ...restProps
+}) => {
+ const inputNode = inputType === 'number' ? : ;
+
+ return (
+
|
+ {editing ? (
+
+ {inputNode}
+
+ ) : (
+ children
+ )}
+ |
+ );
+};
+
+export default EditableCell;
diff --git a/components/EditableTable/index.js b/components/EditableTable/index.js
new file mode 100644
index 0000000..688df75
--- /dev/null
+++ b/components/EditableTable/index.js
@@ -0,0 +1,37 @@
+import { Table, Form } from 'antd';
+import EditableCell from './EditableCell';
+
+const EditableTable = ({ form, isEditing, columns, ...props }) => {
+ const mergedColumns = columns.map((col) => {
+ if (!col.editable) {
+ return col;
+ }
+
+ return {
+ ...col,
+ onCell: (record) => ({
+ record,
+ inputType: col.inputType || 'text',
+ dataIndex: col.dataIndex,
+ title: col.title,
+ editing: isEditing(record)
+ })
+ };
+ });
+
+ return (
+
+ );
+};
+
+export default EditableTable;
diff --git a/components/utils/LinkTo.js b/components/utils/LinkTo.js
new file mode 100644
index 0000000..f3f825e
--- /dev/null
+++ b/components/utils/LinkTo.js
@@ -0,0 +1,29 @@
+// Created by Zack Sheppard (@zackdotcomputer) on 1/19/2021
+// Freely available under MIT License
+// Workaround for https://github.com/vercel/next.js/issues/5533
+import Link from 'next/link';
+
+/// A unified component for the next/link and a standard anchor.
+/// Will lift href and all other props from NextLinkProps up to the Link.
+/// Will automatically make an tag containing the children and pass it remaining props.
+const LinkTo = ({
+ children,
+ href,
+ as,
+ replace,
+ scroll,
+ shallow,
+ prefetch,
+ locale,
+ ...anchorProps
+}) => {
+ return (
+ // These props are lifted up to the `Link` element. All others are passed to the ``
+
+ {/* eslint-disable-next-line react/jsx-props-no-spreading */}
+ {children}
+
+ );
+};
+
+export default LinkTo;
diff --git a/data/forms.yml b/data/forms.yml
deleted file mode 100644
index d438de1..0000000
--- a/data/forms.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-sócios: https://forms.gle/bEaDxAV6e9KrzRZq5
-socios: https://forms.gle/bEaDxAV6e9KrzRZq5
-material: https://forms.gle/yFYeesnBfaDKgoDK9
-recrutamento: https://forms.gle/peu496UmkFqy4sm9A
-feedback: https://form.jotform.com/210226896149360
-dezembro-solidario: https://forms.gle/4MD9DoY9crz4V6va6
-dezembrosolidario: https://forms.gle/4MD9DoY9crz4V6va6
diff --git a/models/Form.js b/models/Form.js
new file mode 100644
index 0000000..b4c8a8f
--- /dev/null
+++ b/models/Form.js
@@ -0,0 +1,12 @@
+import mongoose, { Schema } from 'mongoose';
+
+const Form = new Schema({
+ name: { type: String, required: true },
+ slug: { type: String, unique: true, index: true, required: true },
+ url: { type: String, required: true },
+ visits: { type: Number, default: 0 },
+ created: { type: Date, default: Date.now },
+ updated: { type: Date, default: Date.now }
+});
+
+export default mongoose.models.Form || mongoose.model('Form', Form);
diff --git a/package-lock.json b/package-lock.json
index c27794d..3442b80 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,16 +1,17 @@
{
"name": "cesium.link",
- "version": "2.0.0",
+ "version": "2.0.0-beta",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "version": "2.0.0",
+ "version": "2.0.0-beta",
"dependencies": {
"@ant-design/icons": "^4.4.0",
"@auth0/nextjs-auth0": "^1.1.0",
"animate.css": "^4.1.1",
"antd": "^4.12.2",
+ "luxon": "^1.26.0",
"mongoose": "^5.11.15",
"next": "^10.0.7",
"next-plugin-yaml": "^1.0.1",
@@ -4796,6 +4797,14 @@
"node": ">=10"
}
},
+ "node_modules/luxon": {
+ "version": "1.26.0",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.26.0.tgz",
+ "integrity": "sha512-+V5QIQ5f6CDXQpWNICELwjwuHdqeJM1UenlZWx5ujcRMc9venvluCjFb4t5NYLhb6IhkbMVOxzVuOqkgMxee2A==",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -5283,9 +5292,9 @@
}
},
"node_modules/mongodb": {
- "version": "3.6.4",
- "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.4.tgz",
- "integrity": "sha512-Y+Ki9iXE9jI+n9bVtbTOOdK0B95d6wVGSucwtBkvQ+HIvVdTCfpVRp01FDC24uhC/Q2WXQ8Lpq3/zwtB5Op9Qw==",
+ "version": "3.6.3",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.3.tgz",
+ "integrity": "sha512-rOZuR0QkodZiM+UbQE5kDsJykBqWi0CL4Ec2i1nrGrUI3KO11r6Fbxskqmq3JK2NH7aW4dcccBuUujAP0ERl5w==",
"dependencies": {
"bl": "^2.2.1",
"bson": "^1.1.4",
@@ -5322,17 +5331,17 @@
}
},
"node_modules/mongoose": {
- "version": "5.11.18",
- "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.11.18.tgz",
- "integrity": "sha512-RsrPR9nhkXZbO3ml0DcmdbfeMvFNhgFrP81S6o1P+lFnDTNEKYnGNRCIL+ojD69wj7H5jJaAdZ0SJ5IlKxCHqw==",
+ "version": "5.11.15",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.11.15.tgz",
+ "integrity": "sha512-8T4bT6eCGB7MqCm40oVhnhT/1AyAdwe+y1rYUhdl3ljsks3BpYz8whZgcMkIoh6VoCCjipOXRqZqdk1UByvlYA==",
"dependencies": {
"@types/mongodb": "^3.5.27",
"bson": "^1.1.4",
"kareem": "2.3.2",
- "mongodb": "3.6.4",
+ "mongodb": "3.6.3",
"mongoose-legacy-pluralize": "1.0.2",
"mpath": "0.8.3",
- "mquery": "3.2.4",
+ "mquery": "3.2.3",
"ms": "2.1.2",
"regexp-clone": "1.0.0",
"safe-buffer": "5.2.1",
@@ -5364,9 +5373,9 @@
}
},
"node_modules/mquery": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.4.tgz",
- "integrity": "sha512-uOLpp7iRX0BV1Uu6YpsqJ5b42LwYnmu0WeF/f8qgD/On3g0XDaQM6pfn0m6UxO6SM8DioZ9Bk6xxbWIGHm2zHg==",
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.3.tgz",
+ "integrity": "sha512-cIfbP4TyMYX+SkaQ2MntD+F2XbqaBHUYWk3j+kqdDztPWok3tgyssOZxMHMtzbV1w9DaSlvEea0Iocuro41A4g==",
"dependencies": {
"bluebird": "3.5.1",
"debug": "3.1.0",
@@ -13724,6 +13733,11 @@
"yallist": "^4.0.0"
}
},
+ "luxon": {
+ "version": "1.26.0",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.26.0.tgz",
+ "integrity": "sha512-+V5QIQ5f6CDXQpWNICELwjwuHdqeJM1UenlZWx5ujcRMc9venvluCjFb4t5NYLhb6IhkbMVOxzVuOqkgMxee2A=="
+ },
"make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -14092,9 +14106,9 @@
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"mongodb": {
- "version": "3.6.4",
- "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.4.tgz",
- "integrity": "sha512-Y+Ki9iXE9jI+n9bVtbTOOdK0B95d6wVGSucwtBkvQ+HIvVdTCfpVRp01FDC24uhC/Q2WXQ8Lpq3/zwtB5Op9Qw==",
+ "version": "3.6.3",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.3.tgz",
+ "integrity": "sha512-rOZuR0QkodZiM+UbQE5kDsJykBqWi0CL4Ec2i1nrGrUI3KO11r6Fbxskqmq3JK2NH7aW4dcccBuUujAP0ERl5w==",
"requires": {
"bl": "^2.2.1",
"bson": "^1.1.4",
@@ -14105,17 +14119,17 @@
}
},
"mongoose": {
- "version": "5.11.18",
- "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.11.18.tgz",
- "integrity": "sha512-RsrPR9nhkXZbO3ml0DcmdbfeMvFNhgFrP81S6o1P+lFnDTNEKYnGNRCIL+ojD69wj7H5jJaAdZ0SJ5IlKxCHqw==",
+ "version": "5.11.15",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.11.15.tgz",
+ "integrity": "sha512-8T4bT6eCGB7MqCm40oVhnhT/1AyAdwe+y1rYUhdl3ljsks3BpYz8whZgcMkIoh6VoCCjipOXRqZqdk1UByvlYA==",
"requires": {
"@types/mongodb": "^3.5.27",
"bson": "^1.1.4",
"kareem": "2.3.2",
- "mongodb": "3.6.4",
+ "mongodb": "3.6.3",
"mongoose-legacy-pluralize": "1.0.2",
"mpath": "0.8.3",
- "mquery": "3.2.4",
+ "mquery": "3.2.3",
"ms": "2.1.2",
"regexp-clone": "1.0.0",
"safe-buffer": "5.2.1",
@@ -14135,9 +14149,9 @@
"integrity": "sha512-eb9rRvhDltXVNL6Fxd2zM9D4vKBxjVVQNLNijlj7uoXUy19zNDsIif5zR+pWmPCWNKwAtqyo4JveQm4nfD5+eA=="
},
"mquery": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.4.tgz",
- "integrity": "sha512-uOLpp7iRX0BV1Uu6YpsqJ5b42LwYnmu0WeF/f8qgD/On3g0XDaQM6pfn0m6UxO6SM8DioZ9Bk6xxbWIGHm2zHg==",
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.3.tgz",
+ "integrity": "sha512-cIfbP4TyMYX+SkaQ2MntD+F2XbqaBHUYWk3j+kqdDztPWok3tgyssOZxMHMtzbV1w9DaSlvEea0Iocuro41A4g==",
"requires": {
"bluebird": "3.5.1",
"debug": "3.1.0",
diff --git a/package.json b/package.json
index 3416d7e..a276532 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"@auth0/nextjs-auth0": "^1.1.0",
"animate.css": "^4.1.1",
"antd": "^4.12.2",
+ "luxon": "^1.26.0",
"mongoose": "^5.11.15",
"next": "^10.0.7",
"next-plugin-yaml": "^1.0.1",
diff --git a/pages/[redirect]/[key].js b/pages/[redirect]/[key].js
index 60fc1db..116dfde 100644
--- a/pages/[redirect]/[key].js
+++ b/pages/[redirect]/[key].js
@@ -1,7 +1,7 @@
import ErrorPage from 'next/error';
+import API from '../../utils/api';
import { domain, github, gitlab } from '../../data/settings.yml';
-import forms from '../../data/forms.yml';
const repos = {
gh: github,
@@ -14,10 +14,6 @@ const maps = {
j: 'jobs'
};
-const uris = {
- f: forms
-};
-
export async function getServerSideProps({ params }) {
const { key, redirect } = params;
@@ -39,16 +35,27 @@ export async function getServerSideProps({ params }) {
};
}
- if (redirect in uris) {
- if (!(key in uris[redirect])) {
+ if (redirect === 'f') {
+ const response = await API.get(`/forms/${key}/url`)
+ .then((response) => {
+ return response;
+ })
+ .catch((error) => {
+ return error;
+ });
+
+ if (!response.data.success) {
return {
- notFound: true
+ props: {
+ code: response.status,
+ message: response.data.error.message || response.statusText
+ }
};
}
return {
redirect: {
- destination: `${uris[redirect][key]}`,
+ destination: response.data.data,
permanent: false
}
};
@@ -59,6 +66,8 @@ export async function getServerSideProps({ params }) {
};
}
-const Redirect = () => ;
+const Redirect = ({ message = 'This page could not be found.', code = 404 }) => (
+
+);
export default Redirect;
diff --git a/pages/admin/index.js b/pages/admin/index.js
index 7472bdc..cbfeb34 100644
--- a/pages/admin/index.js
+++ b/pages/admin/index.js
@@ -1,6 +1,7 @@
import { withPageAuthRequired } from '@auth0/nextjs-auth0';
import { AdminContextProvider } from '../../components/Admin/Context';
import LinksTable from '../../components/Admin/LinksTable';
+import FormsTable from '../../components/Admin/FormsTable';
import Navbar, { navbar as entries } from '../../components/Admin/Navbar';
import Footer from '../../components/Footer';
@@ -28,6 +29,7 @@ function Admin({ tab }) {
{(!tab || tab === 'links') && }
+ {(!tab || tab === 'forms') && }
);
diff --git a/pages/api/forms/[slug]/index.js b/pages/api/forms/[slug]/index.js
new file mode 100644
index 0000000..229e129
--- /dev/null
+++ b/pages/api/forms/[slug]/index.js
@@ -0,0 +1,65 @@
+import { withApiAuthRequired } from '@auth0/nextjs-auth0';
+import dbConnect from '../../../../utils/database';
+import Form from '../../../../models/Form';
+
+export default withApiAuthRequired(async (req, res) => {
+ const {
+ query: { slug },
+ method
+ } = req;
+
+ await dbConnect();
+
+ switch (method) {
+ case 'GET':
+ try {
+ const form = await Form.findOne({ slug });
+
+ if (!form) {
+ return res.status(404).json({ success: false, error: { message: 'Form not found' } });
+ }
+ res.status(200).json({ success: true, data: form });
+ } catch (error) {
+ res.status(400).json({ success: false, error: { message: error.message } });
+ }
+ break;
+
+ case 'PUT':
+ try {
+ const form = await Form.findOneAndUpdate(
+ slug,
+ { ...req.body, updated: Date.now() },
+ {
+ new: true,
+ runValidators: true
+ }
+ );
+ if (!form) {
+ return res.status(404).json({ success: false, error: { message: 'Form not found' } });
+ }
+ res.status(200).json({ success: true, data: form });
+ } catch (error) {
+ res.status(400).json({ success: false, error: { message: error.message } });
+ }
+ break;
+
+ case 'DELETE':
+ try {
+ const deleted = await Form.deleteOne({ slug });
+ if (!deleted) {
+ return res.status(400).json({ success: false });
+ }
+ res.status(200).json({ success: true, data: {} });
+ } catch (error) {
+ res.status(400).json({ success: false, error: { message: error.message } });
+ }
+ break;
+ default:
+ res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
+ res.status(405).json({
+ success: false,
+ error: { message: `Method ${method} Not Allowed` }
+ });
+ break;
+ }
+});
diff --git a/pages/api/forms/[slug]/url.js b/pages/api/forms/[slug]/url.js
new file mode 100644
index 0000000..aa97771
--- /dev/null
+++ b/pages/api/forms/[slug]/url.js
@@ -0,0 +1,33 @@
+import dbConnect from '../../../../utils/database';
+import Form from '../../../../models/Form';
+
+export default async (req, res) => {
+ const {
+ query: { slug },
+ method
+ } = req;
+
+ await dbConnect();
+
+ switch (method) {
+ case 'GET':
+ try {
+ const form = await Form.findOneAndUpdate({ slug }, { $inc: { visits: 1 } }, { new: true });
+
+ if (!form) {
+ return res.status(404).json({ success: false, error: { message: 'Form not found' } });
+ }
+ res.status(200).json({ success: true, data: form.url });
+ } catch (error) {
+ res.status(400).json({ success: false, error: { message: error.message } });
+ }
+ break;
+ default:
+ res.setHeader('Allow', ['GET']);
+ res.status(405).json({
+ success: false,
+ error: { message: `Method ${method} Not Allowed` }
+ });
+ break;
+ }
+};
diff --git a/pages/api/forms/index.js b/pages/api/forms/index.js
new file mode 100644
index 0000000..9642ae1
--- /dev/null
+++ b/pages/api/forms/index.js
@@ -0,0 +1,35 @@
+import { withApiAuthRequired } from '@auth0/nextjs-auth0';
+import dbConnect from '../../../utils/database';
+import Form from '../../../models/Form';
+
+export default withApiAuthRequired(async (req, res) => {
+ const { method } = req;
+
+ await dbConnect();
+
+ switch (method) {
+ case 'GET':
+ try {
+ const forms = await Form.find({}).sort({ created: 'asc' });
+ res.status(200).json({ success: true, data: forms });
+ } catch (error) {
+ res.status(400).json({ success: false, error: { message: error.message } });
+ }
+ break;
+ case 'POST':
+ try {
+ const form = await Form.create(req.body);
+ res.status(201).json({ success: true, data: form });
+ } catch (error) {
+ res.status(400).json({ success: false, error: { message: error.message } });
+ }
+ break;
+ default:
+ res.setHeader('Allow', ['GET', 'POST']);
+ res.status(405).json({
+ success: false,
+ error: { message: `Method ${method} Not Allowed` }
+ });
+ break;
+ }
+});
diff --git a/pages/api/links/[id].js b/pages/api/links/[id].js
index f95de67..ba11480 100644
--- a/pages/api/links/[id].js
+++ b/pages/api/links/[id].js
@@ -51,7 +51,7 @@ export default withApiAuthRequired(async (req, res) => {
}
break;
default:
- res.setHeader('Allow', ['POST']);
+ res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).json({
success: false,
error: { message: `Method ${method} Not Allowed` }
diff --git a/pages/api/links/index.js b/pages/api/links/index.js
index 7f11975..af223eb 100644
--- a/pages/api/links/index.js
+++ b/pages/api/links/index.js
@@ -25,7 +25,7 @@ export default withApiAuthRequired(async (req, res) => {
}
break;
default:
- res.setHeader('Allow', ['POST']);
+ res.setHeader('Allow', ['GET', 'POST']);
res.status(405).json({
success: false,
error: { message: `Method ${method} Not Allowed` }