Skip to content

Commit

Permalink
Add shortner feature as redirects tab (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
nelsonmestevao committed Aug 1, 2021
1 parent 891d62c commit 16b61fe
Show file tree
Hide file tree
Showing 19 changed files with 509 additions and 43 deletions.
6 changes: 3 additions & 3 deletions components/Admin/Context/forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ export const reducer = async (forms, action) => {
case 'INIT':
return newForms;
case 'CREATE':
response = await API.post('/forms', form);
response = await API.post('/api/forms', form);
return [...forms, response.data.data];
case 'UPDATE':
response = await API.put(`/forms/${slug}`, form);
response = await API.put(`/api/forms/${slug}`, form);
return forms.map((item) => (item._id === id ? { ...item, ...response.data.data } : item));
case 'DELETE':
await API.delete(`/forms/${slug}`);
await API.delete(`/api/forms/${slug}`);
return forms.filter((form) => form.slug !== slug);
default:
throw new Error(`Unknown action: ${type}`);
Expand Down
7 changes: 6 additions & 1 deletion components/Admin/Context/index.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import useAsyncReducer from '../../../utils/useAsyncReducer';
import { reducer as reducerLinks, LinksContext } from './links';
import { reducer as reducerForms, FormsContext } from './forms';
import { reducer as reducerRedirects, RedirectsContext } from './redirects';

export const AdminContextProvider = ({ children, initialState }) => {
const [links, dispatchLinks] = useAsyncReducer(reducerLinks, initialState);
const [forms, dispatchForms] = useAsyncReducer(reducerForms, initialState);
const [redirects, dispatchRedirects] = useAsyncReducer(reducerRedirects, initialState);

return (
<LinksContext.Provider value={{ links, dispatch: dispatchLinks }}>
<FormsContext.Provider value={{ forms, dispatch: dispatchForms }}>
{children}
<RedirectsContext.Provider value={{ redirects, dispatch: dispatchRedirects }}>
{children}
</RedirectsContext.Provider>
</FormsContext.Provider>
</LinksContext.Provider>
);
};

export { useLinks } from './links';
export { useForms } from './forms';
export { useRedirects } from './redirects';
6 changes: 3 additions & 3 deletions components/Admin/Context/links.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ export const reducer = async (links, action) => {
return newLinks;
case 'CREATE':
link.index = links[links.length - 1].index + 1;
response = await API.post('/links', link);
response = await API.post('/api/links', link);
return [...links, response.data.data];
case 'DELETE':
await API.delete(`/links/${action.id}`);
await API.delete(`/api/links/${action.id}`);
return links.filter((link) => link._id !== action.id);
case 'UPDATE':
return links;
case 'SORT':
if (oldIndex !== newIndex) {
links = arrayMove([].concat(links), oldIndex, newIndex).filter((elem) => !!elem);
links.map((elem, index) => {
API.put(`/links/${elem._id}`, { index });
API.put(`/api/links/${elem._id}`, { index });
});
}
return links;
Expand Down
28 changes: 28 additions & 0 deletions components/Admin/Context/redirects.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useContext, createContext } from 'react';

import API from '../../../utils/api';

export const RedirectsContext = createContext();

export const reducer = async (redirects, action) => {
const { type, slug, redirect, id, redirects: newRedirects } = action;
let response;

switch (type) {
case 'INIT':
return newRedirects;
case 'CREATE':
response = await API.post('/api/redirects', redirect);
return [...redirects, response.data.data];
case 'UPDATE':
response = await API.put(`/api/redirects/${slug}`, form);
return redirects.map((item) => (item._id === id ? { ...item, ...response.data.data } : item));
case 'DELETE':
await API.delete(`/api/redirects/${slug}`);
return redirects.filter((item) => item.slug !== slug);
default:
throw new Error(`Unknown action: ${type}`);
}
};

export const useRedirects = () => useContext(RedirectsContext);
2 changes: 1 addition & 1 deletion components/Admin/FormsTable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function FormsTable() {
const isEditing = (record) => record._id === editing.key;

useEffect(() => {
API.get('/forms')
API.get('/api/forms')
.then((response) => {
dispatch({ type: 'INIT', forms: response.data.data });
setLoading(false);
Expand Down
4 changes: 2 additions & 2 deletions components/Admin/LinksTable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const columns = [
width: 300,
dataIndex: 'slug',
render: function UrlLink(slug) {
const link = `${process.env.NEXT_PUBLIC_APP_URL}/r/${slug}`;
const link = `${process.env.NEXT_PUBLIC_APP_URL}/u/${slug}`;

return (
<Typography.Link href={link} copyable>
Expand Down Expand Up @@ -117,7 +117,7 @@ function LinksTable() {
const { links, dispatch } = useLinks();

useEffect(() => {
API.get('/links')
API.get('/api/links')
.then((response) => {
dispatch({ type: 'INIT', links: response.data.data });
setLoading(false);
Expand Down
4 changes: 2 additions & 2 deletions components/Admin/Navbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const navbar = {
icon: <FormOutlined />,
title: 'Forms'
},
urls: {
redirects: {
icon: <LinkOutlined />,
title: 'Redirects'
}
Expand All @@ -26,7 +26,7 @@ function Navbar({ selected }) {
const [user, setUser] = useState({});

useEffect(() => {
API.get('/auth/me').then((response) => setUser(response.data));
API.get('/api/auth/me').then((response) => setUser(response.data));
}, []);

return (
Expand Down
81 changes: 81 additions & 0 deletions components/Admin/RedirectsTable/Actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useState } from 'react';
import { Button, Popconfirm, Space, notification } from 'antd';
import { CloseOutlined, DeleteOutlined, EditOutlined, SaveOutlined } from '@ant-design/icons';
import { useRedirects } from '../Context';
import { useEditing } from './Context';

function DeleteEntry({ record }) {
const [isVisible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const { dispatch: dispatchRedirects } = useRedirects();

const confirm = () => {
setLoading(true);
dispatchRedirects({ type: 'DELETE', slug: record.slug });
setVisible(false);
setLoading(false);
};

return (
<Popconfirm
title="Are you sure?"
okText="Yes"
cancelText="No"
visible={isVisible}
onConfirm={confirm}
okButtonProps={{ loading: loading }}
onCancel={() => setVisible(false)}>
<Button onClick={() => setVisible(true)} type="link" danger>
<DeleteOutlined />
</Button>
</Popconfirm>
);
}

function Actions({ record }) {
const { redirects, dispatch: dispatchRedirects } = useRedirects();
const { editing, dispatch: dispatchEditing } = useEditing();

const edit = (record) => {
editing.form.setFieldsValue({
name: '',
slug: '',
url: '',
...record
});
dispatchEditing({ type: 'EDIT', key: record._id });
};

const save = async (id) => {
const row = await editing.form.validateFields();
if (redirects.some((elem) => elem.slug === row.slug && row.slug !== record.slug)) {
notification['error']({
message: 'Invalid fields',
description: 'Slug already exists'
});
} else {
dispatchRedirects({ type: 'UPDATE', redirect: row, slug: record.slug, id });
dispatchEditing({ type: 'CANCEL' });
}
};

return record._id === editing.key ? (
<Space>
<Button onClick={() => save(record._id)} type="link">
<SaveOutlined />
</Button>
<Button onClick={() => dispatchEditing({ type: 'CANCEL' })} type="link" danger>
<CloseOutlined />
</Button>
</Space>
) : (
<Space size="middle">
<Button disabled={editing.key !== ''} onClick={() => edit(record)} type="link">
<EditOutlined />
</Button>
<DeleteEntry record={record} />
</Space>
);
}

export default Actions;
18 changes: 18 additions & 0 deletions components/Admin/RedirectsTable/Context/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useContext, createContext } from 'react';

export const EditingContext = createContext();

export const reducer = async (editing, action) => {
const { type, key } = action;

switch (type) {
case 'EDIT':
return { ...editing, key };
case 'CANCEL':
return { ...editing, key: '' };
default:
throw new Error(`Unknown action: ${type}`);
}
};

export const useEditing = () => useContext(EditingContext);
85 changes: 85 additions & 0 deletions components/Admin/RedirectsTable/NewRedirect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useState } from 'react';
import { useRedirects } from '../Context';
import { Modal, Tooltip, Button, Input, Space, Form } from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons';

const { Item } = Form;

function NewRedirect() {
const { redirects, dispatch } = useRedirects();
const [isVisible, setVisible] = useState(false);
const [form] = Form.useForm();

const ok = () => {
form.submit();
setVisible(false);
};

return (
<>
<Button type="primary" onClick={() => setVisible(true)}>
New
</Button>
<Modal title="New Entry" visible={isVisible} onOk={ok} onCancel={() => setVisible(false)}>
<Form
form={form}
onFinish={(values) => dispatch({ type: 'CREATE', redirect: values }) && form.resetFields()}>
<Item
name="name"
label={
<Space>
Name
<Tooltip title="Don't worry, no one can see it.">
<QuestionCircleOutlined />
</Tooltip>
</Space>
}
rules={[
{
required: true,
message: 'Please insert a name.'
}
]}>
<Input placeholder="Title" />
</Item>
<Item
name="slug"
label="Slug"
rules={[
{
message: 'Please insert a slug.'
},
{
validator: async (_, value) => {
if (redirects.some((elem) => elem.slug === value)) {
return Promise.reject(new Error('Slug already exists'));
} else {
Promise.resolve();
}
}
}
]}>
<Input placeholder="slug" />
</Item>
<Item
name="url"
label="URL"
rules={[
{
required: true,
message: 'Please insert a url.'
},
{
type: 'url',
message: 'This field must be a valid url.'
}
]}>
<Input placeholder="https://goo.gl/form" />
</Item>
</Form>
</Modal>
</>
);
}

export default NewRedirect;
Loading

1 comment on commit 16b61fe

@vercel
Copy link

@vercel vercel bot commented on 16b61fe Aug 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.