remove dashboard related code

This commit is contained in:
Matthew Sevey 2022-05-25 10:53:55 -04:00
parent 98d6884391
commit 0041c5cd90
No known key found for this signature in database
GPG Key ID: 9ADDD344F13057F6
42 changed files with 3 additions and 5002 deletions

View File

@ -16,7 +16,6 @@ jobs:
- docker/nginx/Dockerfile - docker/nginx/Dockerfile
- docker/nginx/testing/Dockerfile - docker/nginx/testing/Dockerfile
- docker/sia/Dockerfile - docker/sia/Dockerfile
- packages/dashboard/Dockerfile
- packages/dashboard-v2/Dockerfile - packages/dashboard-v2/Dockerfile
- packages/dnslink-api/Dockerfile - packages/dnslink-api/Dockerfile
- packages/handshake-api/Dockerfile - packages/handshake-api/Dockerfile

View File

@ -1,24 +0,0 @@
name: Lint - packages/dashboard
on:
pull_request:
paths:
- packages/dashboard/**
defaults:
run:
working-directory: packages/dashboard
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16.x
- run: yarn
- run: yarn prettier --check .
- run: yarn next lint

View File

@ -57,9 +57,9 @@ services:
dashboard: dashboard:
# uncomment "build" and comment out "image" to build from sources # uncomment "build" and comment out "image" to build from sources
# build: # build:
# context: https://github.com/SkynetLabs/skynet-webportal.git#master # context: https://github.com/SkynetLabs/webportal-accounts-dashboard.git#master
# dockerfile: ./packages/dashboard/Dockerfile # dockerfile: Dockerfile
image: skynetlabs/dashboard image: skynetlabs/webportal-accounts-dashboard:0.1.0
container_name: dashboard container_name: dashboard
restart: unless-stopped restart: unless-stopped
logging: *default-logging logging: *default-logging

View File

@ -1,6 +0,0 @@
{
"extends": "next/core-web-vitals",
"rules": {
"@next/next/no-html-link-for-pages": "off"
}
}

View File

@ -1,38 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# env defaults
!.env
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
.next

View File

@ -1,3 +0,0 @@
.next
package.json
package-lock.json

View File

@ -1,3 +0,0 @@
{
"printWidth": 120
}

View File

@ -1,20 +0,0 @@
FROM node:16.14.2-alpine
WORKDIR /usr/app
COPY packages/dashboard/package.json \
packages/dashboard/yarn.lock \
./
ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn --frozen-lockfile
COPY packages/dashboard/public ./public
COPY packages/dashboard/src ./src
COPY packages/dashboard/styles ./styles
COPY packages/dashboard/.eslintrc.json \
packages/dashboard/postcss.config.js \
packages/dashboard/tailwind.config.js \
./
CMD ["sh", "-c", "env | grep -E 'NEXT_PUBLIC|STRIPE|ACCOUNTS' > .env.local && yarn build && yarn start"]

View File

@ -1,32 +0,0 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@ -1,15 +0,0 @@
module.exports = {
async redirects() {
const redirects = [];
if (process.env.ACCOUNTS_MAINTENANCE === "true") {
redirects.push({
source: "/((?!maintenance$).*)",
destination: "/maintenance",
permanent: false,
});
}
return redirects;
},
};

View File

@ -1,44 +0,0 @@
{
"name": "dashboard",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@fontsource/sora": "4.5.8",
"@fontsource/source-sans-pro": "4.5.9",
"@stripe/react-stripe-js": "1.7.2",
"@stripe/stripe-js": "1.29.0",
"classnames": "2.3.1",
"copy-text-to-clipboard": "^3.0.1",
"dayjs": "1.11.1",
"express-jwt": "7.6.2",
"fast-levenshtein": "3.0.0",
"formik": "2.2.9",
"http-status-codes": "2.2.0",
"ky": "0.30.0",
"next": "12.1.6",
"normalize.css": "8.0.1",
"pretty-bytes": "6.0.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-toastify": "9.0.0",
"skynet-js": "3.0.2",
"stripe": "8.220.0",
"swr": "1.3.0",
"yup": "0.32.11"
},
"devDependencies": {
"@tailwindcss/forms": "0.5.1",
"@tailwindcss/typography": "0.5.2",
"autoprefixer": "10.4.7",
"eslint": "8.14.0",
"eslint-config-next": "12.1.6",
"postcss": "8.4.13",
"prettier": "2.6.2",
"tailwindcss": "3.0.24"
}
}

View File

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,62 +0,0 @@
const types = {
error: {
backgroundColor: "bg-red-50",
titleColor: "text-red-800",
detailsColor: "text-red-700",
iconColor: "text-red-400",
icon: (
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
),
},
info: {
backgroundColor: "bg-blue-50",
titleColor: "text-blue-800",
detailsColor: "text-blue-700",
iconColor: "text-blue-400",
icon: (
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
),
},
};
export default function Message({ type = "info", title, items = [] }) {
const { backgroundColor, titleColor, detailsColor, iconColor, icon } = types[type];
return (
<div className={`rounded-md ${backgroundColor} p-4`}>
<div className="flex">
<div className="flex-shrink-0">
<svg
className={`h-5 w-5 ${iconColor}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
{icon}
</svg>
</div>
<div className="ml-3">
{title && <h3 className={`text-sm font-medium ${titleColor}`}>{title}</h3>}
{items.length > 0 && (
<div className={`${title ? "mt-2" : ""} text-sm ${detailsColor}`}>
<ul className={`${items.length > 1 ? "list-disc pl-5 space-y-1" : ""}`}>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
)}
</div>
</div>
</div>
);
}

View File

@ -1,93 +0,0 @@
import * as React from "react";
import { useFormik, getIn, setIn } from "formik";
import classnames from "classnames";
import SelfServiceMessages from "./SelfServiceMessages";
export default function SelfServiceForm({
fieldsConfig,
onSubmit,
title,
onError,
validationSchema = null,
button = "Submit",
}) {
const [messages, setMessages] = React.useState([]);
const fields = fieldsConfig.sort((a, b) => (a.position < b.position ? -1 : 1));
const formik = useFormik({
initialValues: fields.reduce((acc, field) => setIn(acc, field.name, field.value ?? ""), {}),
validationSchema,
onSubmit: async (values) => {
if (!formik.isValid) return;
setMessages([]);
try {
await onSubmit(values);
} catch (error) {
if (error.response) {
const data = await error.response.json();
if (data.message) {
if (typeof onError === "function") {
onError(data.message);
}
setMessages((messages) => [...messages, { type: "error", text: data.message }]);
}
} else {
setMessages((messages) => [...messages, { type: "error", text: error.toString() }]);
}
}
},
});
return (
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
{title && <h3 className="pb-5 text-lg leading-6 font-medium text-gray-900">{title}</h3>}
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form className="space-y-6" onSubmit={formik.handleSubmit}>
{fields.map((field) => (
<div key={field.name} className={classnames({ hidden: field.type === "hidden" })}>
<label htmlFor={field.name} className="block text-sm font-medium text-gray-700 mb-1">
{field.label ?? field.name}
</label>
<div>
<input
id={field.type === "hidden" ? null : field.name}
name={field.name}
type={field.type}
autoComplete={field.autoComplete}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={getIn(formik.values, field.name) ?? ""}
className={classnames(
"appearance-none block w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none sm:text-sm",
{
"border-gray-300 placeholder-gray-400 focus:ring-green-500 focus:border-green-500": !(
formik.errors[field.name] && formik.touched[field.name]
),
"border-red-300 text-red-900 placeholder-red-300 focus:ring-red-500 focus:border-red-500":
formik.errors[field.name] && formik.touched[field.name],
}
)}
/>
{formik.errors[field.name] && formik.touched[field.name] && (
<p className="mt-2 text-xs text-red-600">{formik.errors[field.name]}</p>
)}
</div>
</div>
))}
<button
type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
{button}
</button>
{messages.length > 0 && <SelfServiceMessages messages={messages} />}
</form>
</div>
</div>
);
}

View File

@ -1,46 +0,0 @@
import classnames from "classnames";
// const types = {
// error: {
// backgroundColor: "bg-red-50",
// titleColor: "text-red-800",
// detailsColor: "text-red-700",
// iconColor: "text-red-400",
// icon: (
// <path
// fillRule="evenodd"
// d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
// clipRule="evenodd"
// />
// ),
// },
// info: {
// backgroundColor: "bg-blue-50",
// titleColor: "text-blue-800",
// detailsColor: "text-blue-700",
// iconColor: "text-blue-400",
// icon: (
// <path
// fillRule="evenodd"
// d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
// clipRule="evenodd"
// />
// ),
// },
// };
export default function SelfServiceMessages({ messages = [] }) {
if (!messages) return null; // make sure we don't throw on invalid data
return messages.map(({ text, type }, index) => (
<p
key={index}
className={classnames("mt-2 text-sm", {
"text-red-600": type === "error",
"text-blue-600": type === "info",
})}
>
{text}
</p>
));
}

View File

@ -1,317 +0,0 @@
import Link from "next/link";
import { useRouter } from "next/router";
import Head from "next/head";
import { useState } from "react";
import accountsApi from "../services/accountsApi";
export default function Layout({ title, children }) {
const [menuOpen, openMenu] = useState(false);
const [avatarDropdownOpen, openAvatarDropdown] = useState(false);
const router = useRouter();
const handleSignOut = async (e) => {
e.preventDefault();
try {
await accountsApi.post("logout");
router.push("/auth/login");
} catch (error) {
console.log(error); // todo: handle errors with a message
}
};
return (
<div>
<Head>
<title key="title">Skynet - {title}</title>
</Head>
<div className="bg-gray-800 pb-32">
<nav className="bg-gray-800">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="border-b border-gray-700">
<div className="flex items-center justify-between h-16 px-4 sm:px-0">
<div className="flex items-center">
<Link href="/">
<a className="flex-shrink-0">
<svg
role="img"
width="33"
height="33"
fill="#00C65E"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Skynet</title>
<path d="m-.0004 6.4602 21.3893 11.297c.561.2935.6633 1.0532.1999 1.4846h-.011a10.0399 10.0399 0 0 1-2.2335 1.5307c-6.912 3.4734-14.9917-1.838-14.5438-9.5605l2.8601 1.9752c.856 4.508 5.6187 7.1094 9.8742 5.3932zm8.6477 3.1509 14.3661 5.6785a.8704.8704 0 0 1 .5197 1.0466v.0182c-.1537.5377-.7668.7938-1.2575.5252zm5.2896-7.4375c2.7093-.2325 6.0946.7869 8.1116 3.3871 1.699 2.1951 2.0497 4.8772 1.9298 7.6465v-.007c-.0478.5874-.6494.9616-1.1975.745l-9.7652-3.8596 9.0656 2.4313a7.296 7.296 0 0 0-1.0677-4.5631c-2.9683-4.7678-9.9847-4.5344-12.6297.4201a7.5048 7.5048 0 0 0-.398.8831L5.5546 7.9614c.069-.1017.1417-.198.2144-.2962.1163-.2416.2417-.487.3798-.7268 1.6118-2.7911 4.3102-4.4338 7.1558-4.6973.2108-.0182.4215-.049.6323-.0672z" />
</svg>
</a>
</Link>
<div className="hidden md:block">
<div className="ml-10 flex items-baseline space-x-4">
{/* Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" */}
<Link href="/">
<a
className={`${
router.pathname === "/"
? "bg-gray-900 text-white"
: "text-gray-300 hover:bg-gray-700 hover:text-white"
} px-3 py-2 rounded-md text-sm font-medium`}
>
Dashboard
</a>
</Link>
<Link href="/uploads">
<a
className={`${
router.pathname === "/uploads"
? "bg-gray-900 text-white"
: "text-gray-300 hover:bg-gray-700 hover:text-white"
} px-3 py-2 rounded-md text-sm font-medium`}
>
Your uploads
</a>
</Link>
<Link href="/downloads">
<a
className={`${
router.pathname === "/downloads"
? "bg-gray-900 text-white"
: "text-gray-300 hover:bg-gray-700 hover:text-white"
} px-3 py-2 rounded-md text-sm font-medium`}
>
Your downloads
</a>
</Link>
<a
href={`https://${process.env.NEXT_PUBLIC_PORTAL_DOMAIN}`}
className="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium flex items-center"
target="_blank"
rel="noopener noreferrer"
>
Upload files
<svg
className="flex-shrink-0 h-4 w-4 ml-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
</div>
</div>
</div>
<div className="hidden md:block">
<div className="ml-4 flex items-center md:ml-6">
{/* Profile dropdown */}
<div className="ml-3 relative">
<div>
<button
className="max-w-xs bg-gray-800 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
id="user-menu"
aria-haspopup="true"
onClick={() => openAvatarDropdown(!avatarDropdownOpen)}
>
<span className="sr-only">Open user menu</span>
<span className="inline-block h-8 w-8 rounded-full overflow-hidden bg-gray-100">
<svg className="h-full w-full text-gray-300" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</span>
</button>
</div>
{/*
Profile dropdown panel, show/hide based on dropdown state.
Entering: "transition ease-out duration-100"
From: "transform opacity-0 scale-95"
To: "transform opacity-100 scale-100"
Leaving: "transition ease-in duration-75"
From: "transform opacity-100 scale-100"
To: "transform opacity-0 scale-95"
*/}
{avatarDropdownOpen && (
<div
className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5"
role="menu"
aria-orientation="vertical"
aria-labelledby="user-menu"
>
<Link href="/user/settings">
<a className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem">
Settings
</a>
</Link>
<Link href="/payments">
<a className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem">
Payments
</a>
</Link>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer"
role="menuitem"
onClick={handleSignOut}
>
Sign out
</a>
</div>
)}
</div>
</div>
</div>
<div className="-mr-2 flex md:hidden">
{/* Mobile menu button */}
<button
className="bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
onClick={() => openMenu(!menuOpen)}
>
<span className="sr-only">Open main menu</span>
<svg
className={`${menuOpen ? "hidden" : "block"} h-6 w-6`}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
<svg
className={`${menuOpen ? "block" : "hidden"} h-6 w-6`}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
</div>
<div className={`${menuOpen ? "block" : "hidden"} border-b border-gray-700 md:hidden`}>
<div className="px-2 py-3 space-y-1 sm:px-3">
{/* Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" */}
<Link href="/">
<a
className={`${
router.pathname === "/"
? "bg-gray-900 text-white"
: "text-gray-300 hover:bg-gray-700 hover:text-white"
} block px-3 py-2 rounded-md text-base font-medium`}
>
Dashboard
</a>
</Link>
<Link href="/uploads">
<a
className={`${
router.pathname === "/uploads"
? "bg-gray-900 text-white"
: "text-gray-300 hover:bg-gray-700 hover:text-white"
} block px-3 py-2 rounded-md text-base font-medium`}
>
Your uploads
</a>
</Link>
<Link href="/downloads">
<a
className={`${
router.pathname === "/downloads"
? "bg-gray-900 text-white"
: "text-gray-300 hover:bg-gray-700 hover:text-white"
} block px-3 py-2 rounded-md text-base font-medium`}
>
Your downloads
</a>
</Link>
<a
href={`https://${process.env.NEXT_PUBLIC_PORTAL_DOMAIN}`}
className="text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium flex items-center"
target="_blank"
rel="noopener noreferrer"
>
Upload files
<svg
className="flex-shrink-0 h-4 w-4 ml-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
</div>
<div className="pt-4 pb-3 border-t border-gray-700">
{/* <div className="flex items-center px-5">
<div className="flex-shrink-0">
<span className="inline-block h-10 w-10 rounded-full overflow-hidden bg-gray-100">
<svg className="h-full w-full text-gray-300" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</span>
</div>
<div className="ml-3">
<div className="text-base font-medium leading-none text-white">John Doe</div>
<div className="text-sm font-medium leading-none text-gray-400">john@example.com</div>
</div>
</div> */}
<div className="mt-3 px-2 space-y-1">
<Link href="/user/settings">
<a className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700">
Settings
</a>
</Link>
<Link href="/payments">
<a className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700">
Payments
</a>
</Link>
<a
href="#"
onClick={handleSignOut}
className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700 cursor-pointer"
>
Sign out
</a>
</div>
</div>
</div>
</nav>
<header className="py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold text-white">{title}</h1>
</div>
</header>
</div>
<main className="-mt-32">
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
{children || (
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6">
<div className="border-4 border-dashed border-gray-200 rounded-lg h-96" />
</div>
)}
</div>
</main>
<footer className="max-w-7xl mx-auto py-4 sm:py-6 px-4 sm:px-6 md:flex md:items-center md:justify-between lg:px-8">
<p className="text-center text-sm text-gray-400">© 2021 Skynet Labs Inc. All rights reserved.</p>
</footer>
</div>
);
}

View File

@ -1,140 +0,0 @@
import { useEffect } from "react";
import classnames from "classnames";
function Button({ children, disabled, ...props }) {
return (
<button
type="button"
className={classnames(
"inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white",
{
"hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500": !disabled,
"cursor-auto opacity-50": disabled,
}
)}
disabled={disabled}
{...props}
>
{children}
</button>
);
}
function ButtonAction({ children, disabled, ...props }) {
return (
<button
type="button"
className={classnames(
"inline-flex items-center px-2.5 py-1.5 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white",
{
"hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500": !disabled,
"cursor-auto opacity-50": disabled,
}
)}
disabled={disabled}
{...props}
>
{children}
</button>
);
}
export default function Table({ items, count, headers, mutate, actions, offset, setOffset, pageSize = 10 }) {
useEffect(() => {
if (offset < 0) setOffset(0);
else if (offset >= count && count > 0) setOffset(Math.floor(count / pageSize - 1) * pageSize);
else if (offset % pageSize) setOffset(offset - (offset % pageSize));
}, [count, offset, pageSize, setOffset]);
return (
<div className="flex flex-col">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
{headers.map(({ key, name }) => (
<th
key={key}
scope="col"
className="px-6 py-3 whitespace-nowrap text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
{name}
</th>
))}
{actions.map(({ name }, index) => (
<th key={index} scope="col" className="relative px-6 py-3">
<span className="sr-only">{name}</span>
</th>
))}
</tr>
</thead>
<tbody>
{items && items.length ? (
items.map((row, index) => (
<tr className={index % 2 ? "bg-gray-100" : "bg-white"} key={index}>
{headers.map(({ key, formatter, href, nowrap }) => (
<td
key={key}
className={`${nowrap ? "whitespace-nowrap" : ""} px-6 py-4 text-sm font-medium text-gray-900`}
>
{(formatter ? (
formatter(row, key)
) : href ? (
<a
href={href(row, key)}
className="text-green-600 hover:text-green-900"
target="_blank"
rel="noopener noreferrer"
>
{row[key]}
</a>
) : (
row[key]
)) || <>&mdash;</>}
</td>
))}
{actions.map(({ name, action }, index) => (
<td key={index} className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<ButtonAction onClick={() => action(row, mutate)}>{name}</ButtonAction>
</td>
))}
</tr>
))
) : (
<tr className="bg-white">
<td colSpan={headers.length + actions.length} className="text-center py-6 text-sm text-gray-500">
no entries
</td>
</tr>
)}
</tbody>
</table>
{/* This example requires Tailwind CSS v2.0+ */}
<nav
className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6"
aria-label="Pagination"
>
<div className="hidden sm:block">
<p className="text-sm text-gray-700">
Showing <span className="font-medium">{count ? offset + 1 : 0}</span> to{" "}
<span className="font-medium">{offset + pageSize > count ? count : offset + pageSize}</span> of{" "}
<span className="font-medium">{count}</span> results
</p>
</div>
<div className="flex-1 flex justify-between sm:justify-end space-x-3">
<Button disabled={offset - pageSize < 0} onClick={() => setOffset(offset - pageSize)}>
Previous
</Button>
<Button disabled={offset + pageSize >= count} onClick={() => setOffset(offset + pageSize)}>
Next
</Button>
</div>
</nav>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,5 +0,0 @@
export default {
tiers: {
starter: { id: "starter", tier: 1, name: "Free", description: "Pin up to 100GB" },
},
};

View File

@ -1,30 +0,0 @@
import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
import { ToastContainer } from "react-toastify";
import Head from "next/head";
import "normalize.css";
import "react-toastify/dist/ReactToastify.css";
import "../../styles/globals.css";
import "@fontsource/sora/300.css"; // light
import "@fontsource/sora/400.css"; // normal
import "@fontsource/sora/500.css"; // medium
import "@fontsource/sora/600.css"; // semibold
import "@fontsource/source-sans-pro/400.css"; // normal
import "@fontsource/source-sans-pro/600.css"; // semibold
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
function MyApp({ Component, pageProps }) {
return (
<Elements stripe={stripePromise}>
<Head>
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<title key="title">Skynet</title>
</Head>
<Component {...pageProps} />
<ToastContainer bodyClassName={() => "Toastify__toast-body text-sm font-medium text-palette-500"} />
</Elements>
);
}
export default MyApp;

View File

@ -1,28 +0,0 @@
import ky from "ky";
import Stripe from "stripe";
import { StatusCodes } from "http-status-codes";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const getStripeCustomer = (stripeCustomerId = null) => {
if (stripeCustomerId) {
return stripe.customers.retrieve(stripeCustomerId);
}
return stripe.customers.create();
};
export default async function billingApi(req, res) {
try {
const cookie = req.headers.cookie; // cookie header from request
const { stripeCustomerId } = await ky("http://accounts:3000/user", { headers: { cookie } }).json();
const customer = await getStripeCustomer(stripeCustomerId);
const session = await stripe.billingPortal.sessions.create({
customer: customer.id,
return_url: `https://account.${process.env.PORTAL_DOMAIN}/payments`,
});
res.redirect(session.url);
} catch ({ message }) {
res.status(StatusCodes.BAD_REQUEST).json({ error: { message } });
}
}

View File

@ -1,58 +0,0 @@
import ky from "ky";
import Stripe from "stripe";
import { StatusCodes } from "http-status-codes";
import { isPaidTier } from "../../../services/tiers";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const getStripeCustomer = async (user, cookie) => {
if (user.stripeCustomerId) {
return stripe.customers.retrieve(user.stripeCustomerId);
}
const customer = await stripe.customers.create();
// update user instance and include the customer id once created
await ky.put("http://accounts:3000/user", { headers: { cookie }, json: { stripeCustomerId: customer.id } });
return customer;
};
export default async function checkoutApi(req, res) {
if (req.method !== "POST") {
return res.status(StatusCodes.NOT_FOUND).end();
}
const { price } = req.body;
if (!price) {
return res.status(StatusCodes.BAD_REQUEST).json({ error: { message: "Missing 'price' attribute" } });
}
try {
const cookie = req.headers.cookie; // cookie header from request
const user = await ky("http://accounts:3000/user", { headers: { cookie } }).json();
if (isPaidTier(user.tier)) {
const message = `Customer can have only one active subscription at a time, use Stripe Customer Portal to manage active subscription`;
return res.status(StatusCodes.BAD_REQUEST).json({ error: { message } });
}
const customer = await getStripeCustomer(user, cookie);
const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
line_items: [{ price, quantity: 1 }],
customer: customer.id,
client_reference_id: user.sub,
allow_promotion_codes: true,
success_url: `https://account.${process.env.PORTAL_DOMAIN}/payments?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `https://account.${process.env.PORTAL_DOMAIN}/payments`,
});
res.json({ sessionId: session.id });
} catch (error) {
res.status(StatusCodes.BAD_REQUEST).json({ error: { message: error.message } });
}
}

View File

@ -1,86 +0,0 @@
import Link from "next/link";
import { useRouter } from "next/router";
import * as Yup from "yup";
import accountsApi from "../../services/accountsApi";
import useAnonRoute from "../../services/useAnonRoute";
import SelfServiceForm from "../../components/Form/SelfServiceForm";
const fieldsConfig = [
{
name: "email",
type: "text",
label: "Email address",
autoComplete: "email",
position: 0,
},
{
name: "password",
type: "password",
label: "Password",
autoComplete: "current-password",
position: 1,
},
];
const validationSchema = Yup.object().shape({
email: Yup.string().required("Email is required").email("This email is invalid"),
password: Yup.string().required("Password is required"),
});
export default function Login() {
useAnonRoute(); // ensure user is not logged in
const router = useRouter();
const onSubmit = async (values) => {
await accountsApi.post("login", {
json: {
email: values.email,
password: values.password,
},
});
router.push("/");
};
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<svg
xmlns="http://www.w3.org/2000/svg"
width="169"
height="39"
viewBox="0 0 169 39"
className="mx-auto h-12 w-auto"
>
<path
d="M160.701 31.245c-2.736 0-4.788-.706-6.156-2.118-1.369-1.411-2.053-3.424-2.053-6.039V2.674h7.487v6.33H169v6.15h-9.02v7.355c0 1.753.886 2.63 2.66 2.63h6.134v6.106h-8.073zm-24.942 0c-1.744 0-3.352-.268-4.826-.803-1.473-.535-2.751-1.292-3.833-2.273a10.275 10.275 0 01-2.526-3.521c-.602-1.367-.902-2.882-.902-4.546 0-1.635.3-3.135.902-4.502a10.275 10.275 0 012.526-3.521c1.082-.98 2.36-1.738 3.833-2.273 1.474-.535 3.082-.803 4.826-.803h5.728c1.293 0 2.42.179 3.383.535.962.357 1.767.847 2.413 1.471a5.823 5.823 0 011.443 2.251c.316.877.474 1.835.474 2.875 0 1.961-.571 3.432-1.714 4.412-1.143.981-3.022 1.471-5.638 1.471h-8.028v-4.056h6.45c1.172 0 1.759-.52 1.759-1.56 0-1.1-.572-1.649-1.714-1.649h-4.6c-1.354 0-2.451.46-3.293 1.382-.842.921-1.263 2.228-1.263 3.922s.42 2.964 1.263 3.811c.842.847 1.94 1.27 3.292 1.27h12.268v6.107H135.76zm-22.867 0V19.567c0-2.526-1.308-3.789-3.924-3.789h-7.532v15.467H93.86V9.003h15.29c3.728 0 6.54.922 8.434 2.764 1.894 1.842 2.841 4.457 2.841 7.844v11.634h-7.532zM67.293 39v-6.418h2.39c.963 0 1.715-.193 2.256-.58a4.58 4.58 0 001.308-1.426L62.422 9.003h7.623l6.675 14.263 6.855-14.263h7.668L78.749 33.43c-.48.95-.984 1.775-1.51 2.473a7.85 7.85 0 01-1.782 1.739 7.11 7.11 0 01-2.233 1.025c-.827.223-1.781.334-2.864.334h-3.067zm-14.207-7.755l-7.713-9.316a4.647 4.647 0 01-.54-.914 2.306 2.306 0 01-.181-.913v-.535c0-.654.225-1.278.676-1.872l7.487-8.692h8.48l-9.02 10.787 9.47 11.455h-8.66zm-17.41 0V0h7.532v31.245h-7.532zm-35.315 0v-7.488h21.92c.571 0 1.037-.171 1.398-.513.36-.342.541-.825.541-1.449 0-.624-.18-1.114-.541-1.47-.36-.357-.827-.535-1.398-.535H9.38c-1.353 0-2.608-.216-3.766-.647-1.157-.43-2.15-1.025-2.976-1.782a8.005 8.005 0 01-1.94-2.742C.233 13.55 0 12.361 0 11.054 0 9.746.233 8.55.7 7.466A8.184 8.184 0 012.638 4.68c.826-.773 1.819-1.367 2.976-1.783 1.158-.416 2.413-.624 3.766-.624h22.281v7.444H9.742c-.571 0-1.03.163-1.375.49-.346.327-.52.802-.52 1.426 0 .594.174 1.07.52 1.426.345.357.804.535 1.375.535h12.9c1.383 0 2.646.216 3.788.647a9.097 9.097 0 012.977 1.805 8.038 8.038 0 011.962 2.785c.466 1.085.7 2.266.7 3.544 0 1.337-.234 2.548-.7 3.632a8.267 8.267 0 01-1.962 2.808 8.62 8.62 0 01-2.977 1.806c-1.142.416-2.405.624-3.788.624H.36z"
fill="#57B560"
fillRule="evenodd"
/>
</svg>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Sign in to your account</h2>
<p className="mt-2 text-center text-sm text-gray-600 max-w">
or{" "}
<Link href="/auth/registration">
<a className="font-medium text-green-600 hover:text-green-500">sign up</a>
</Link>{" "}
if you don&apos;t have one yet
</p>
</div>
<SelfServiceForm
fieldsConfig={fieldsConfig}
validationSchema={validationSchema}
onSubmit={onSubmit}
button="Sign in"
/>
<div className="sm:mx-auto sm:w-full sm:max-w-md text-center mt-2">
<Link href="/auth/recovery">
<a className="text-sm font-medium text-green-600 hover:text-green-500">Forgot your password?</a>
</Link>
</div>
</div>
);
}

View File

@ -1,100 +0,0 @@
import * as React from "react";
import Link from "next/link";
import * as Yup from "yup";
import accountsApi from "../../services/accountsApi";
import useAnonRoute from "../../services/useAnonRoute";
import SelfServiceForm from "../../components/Form/SelfServiceForm";
const fieldsConfig = [
{
name: "email",
type: "text",
label: "Email address",
autoComplete: "email",
position: 0,
},
];
const validationSchema = Yup.object().shape({
email: Yup.string().required("Email is required").email("This email is invalid"),
});
export default function Recovery() {
useAnonRoute(); // ensure user is not logged in
const [success, setSuccess] = React.useState(false);
const [skynetFreeInviteVisible, setSkynetFreeInviteVisible] = React.useState(false);
const isSiaskyNet = typeof window !== "undefined" && window.location.hostname === "account.siasky.net";
const onSubmit = async (values) => {
await accountsApi.post("user/recover/request", {
json: {
email: values.email,
},
});
setSuccess(true);
};
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<svg
xmlns="http://www.w3.org/2000/svg"
width="169"
height="39"
viewBox="0 0 169 39"
className="mx-auto h-12 w-auto"
>
<path
d="M160.701 31.245c-2.736 0-4.788-.706-6.156-2.118-1.369-1.411-2.053-3.424-2.053-6.039V2.674h7.487v6.33H169v6.15h-9.02v7.355c0 1.753.886 2.63 2.66 2.63h6.134v6.106h-8.073zm-24.942 0c-1.744 0-3.352-.268-4.826-.803-1.473-.535-2.751-1.292-3.833-2.273a10.275 10.275 0 01-2.526-3.521c-.602-1.367-.902-2.882-.902-4.546 0-1.635.3-3.135.902-4.502a10.275 10.275 0 012.526-3.521c1.082-.98 2.36-1.738 3.833-2.273 1.474-.535 3.082-.803 4.826-.803h5.728c1.293 0 2.42.179 3.383.535.962.357 1.767.847 2.413 1.471a5.823 5.823 0 011.443 2.251c.316.877.474 1.835.474 2.875 0 1.961-.571 3.432-1.714 4.412-1.143.981-3.022 1.471-5.638 1.471h-8.028v-4.056h6.45c1.172 0 1.759-.52 1.759-1.56 0-1.1-.572-1.649-1.714-1.649h-4.6c-1.354 0-2.451.46-3.293 1.382-.842.921-1.263 2.228-1.263 3.922s.42 2.964 1.263 3.811c.842.847 1.94 1.27 3.292 1.27h12.268v6.107H135.76zm-22.867 0V19.567c0-2.526-1.308-3.789-3.924-3.789h-7.532v15.467H93.86V9.003h15.29c3.728 0 6.54.922 8.434 2.764 1.894 1.842 2.841 4.457 2.841 7.844v11.634h-7.532zM67.293 39v-6.418h2.39c.963 0 1.715-.193 2.256-.58a4.58 4.58 0 001.308-1.426L62.422 9.003h7.623l6.675 14.263 6.855-14.263h7.668L78.749 33.43c-.48.95-.984 1.775-1.51 2.473a7.85 7.85 0 01-1.782 1.739 7.11 7.11 0 01-2.233 1.025c-.827.223-1.781.334-2.864.334h-3.067zm-14.207-7.755l-7.713-9.316a4.647 4.647 0 01-.54-.914 2.306 2.306 0 01-.181-.913v-.535c0-.654.225-1.278.676-1.872l7.487-8.692h8.48l-9.02 10.787 9.47 11.455h-8.66zm-17.41 0V0h7.532v31.245h-7.532zm-35.315 0v-7.488h21.92c.571 0 1.037-.171 1.398-.513.36-.342.541-.825.541-1.449 0-.624-.18-1.114-.541-1.47-.36-.357-.827-.535-1.398-.535H9.38c-1.353 0-2.608-.216-3.766-.647-1.157-.43-2.15-1.025-2.976-1.782a8.005 8.005 0 01-1.94-2.742C.233 13.55 0 12.361 0 11.054 0 9.746.233 8.55.7 7.466A8.184 8.184 0 012.638 4.68c.826-.773 1.819-1.367 2.976-1.783 1.158-.416 2.413-.624 3.766-.624h22.281v7.444H9.742c-.571 0-1.03.163-1.375.49-.346.327-.52.802-.52 1.426 0 .594.174 1.07.52 1.426.345.357.804.535 1.375.535h12.9c1.383 0 2.646.216 3.788.647a9.097 9.097 0 012.977 1.805 8.038 8.038 0 011.962 2.785c.466 1.085.7 2.266.7 3.544 0 1.337-.234 2.548-.7 3.632a8.267 8.267 0 01-1.962 2.808 8.62 8.62 0 01-2.977 1.806c-1.142.416-2.405.624-3.788.624H.36z"
fill="#57B560"
fillRule="evenodd"
/>
</svg>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Recover your account</h2>
<p className="mt-2 text-center text-sm text-gray-600 max-w">
<Link href="/auth/login">
<a className="font-medium text-green-600 hover:text-green-500">sign in</a>
</Link>{" "}
if you suddenly remembered your password
</p>
<p className="mt-2 text-center text-sm text-gray-600 max-w">
or{" "}
<Link href="/auth/registration">
<a className="font-medium text-green-600 hover:text-green-500">sign up</a>
</Link>{" "}
for a new account
</p>
{skynetFreeInviteVisible && isSiaskyNet && (
<div className="font-content rounded border border-blue-200 mt-6 p-4 bg-blue-100">
<p>
All Siasky.net accounts have been moved to{" "}
<a className="text-primary" href="https://skynetfree.net">
SkynetFree.net
</a>
</p>
</div>
)}
</div>
{!success && (
<SelfServiceForm
fieldsConfig={fieldsConfig}
validationSchema={validationSchema}
onSubmit={onSubmit}
button="Send recovery link"
onError={(errorMessage) =>
setSkynetFreeInviteVisible(errorMessage === "registrations are currently disabled")
}
/>
)}
{success && (
<p className="mt-4 text-center text-primary">
Account recovery requested, please follow instructions sent in email.
</p>
)}
</div>
);
}

View File

@ -1,98 +0,0 @@
import Link from "next/link";
import { useRouter } from "next/router";
import levenshtein from "fast-levenshtein";
import * as Yup from "yup";
import accountsApi from "../../services/accountsApi";
import useAnonRoute from "../../services/useAnonRoute";
import lcs from "../../services/longestCommonSequence";
import SelfServiceForm from "../../components/Form/SelfServiceForm";
const fieldsConfig = [
{
name: "email",
type: "text",
label: "Email address",
autoComplete: "email",
position: 0,
},
{
name: "password",
type: "password",
label: "Password",
autoComplete: "new-password",
position: 1,
},
{
name: "confirmPassword",
type: "password",
label: "Password repeated",
autoComplete: "new-password",
position: 2,
},
];
const validationSchema = Yup.object().shape({
email: Yup.string().required("Email is required").email("This email is invalid"),
password: Yup.string()
.required("Password is required")
.min(6, "Password has to be at least 6 characters long")
.test("levenshtein", "This password is too similar to your email", function (value) {
const email = this.parent.email;
// levenshtein distance higher than 5 and longest common sequence shorter than half of the password
return value && email && levenshtein.get(value, email) > 5 && lcs(value, email).length / value.length <= 0.5;
}),
confirmPassword: Yup.string().oneOf([Yup.ref("password"), null], "Passwords must match"),
});
export default function Registration() {
useAnonRoute(); // ensure user is not logged in
const router = useRouter();
const onSubmit = async (values) => {
await accountsApi.post("user", {
json: {
email: values.email,
password: values.password,
},
});
router.push("/");
};
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<svg
xmlns="http://www.w3.org/2000/svg"
width="169"
height="39"
viewBox="0 0 169 39"
className="mx-auto h-12 w-auto"
>
<path
d="M160.701 31.245c-2.736 0-4.788-.706-6.156-2.118-1.369-1.411-2.053-3.424-2.053-6.039V2.674h7.487v6.33H169v6.15h-9.02v7.355c0 1.753.886 2.63 2.66 2.63h6.134v6.106h-8.073zm-24.942 0c-1.744 0-3.352-.268-4.826-.803-1.473-.535-2.751-1.292-3.833-2.273a10.275 10.275 0 01-2.526-3.521c-.602-1.367-.902-2.882-.902-4.546 0-1.635.3-3.135.902-4.502a10.275 10.275 0 012.526-3.521c1.082-.98 2.36-1.738 3.833-2.273 1.474-.535 3.082-.803 4.826-.803h5.728c1.293 0 2.42.179 3.383.535.962.357 1.767.847 2.413 1.471a5.823 5.823 0 011.443 2.251c.316.877.474 1.835.474 2.875 0 1.961-.571 3.432-1.714 4.412-1.143.981-3.022 1.471-5.638 1.471h-8.028v-4.056h6.45c1.172 0 1.759-.52 1.759-1.56 0-1.1-.572-1.649-1.714-1.649h-4.6c-1.354 0-2.451.46-3.293 1.382-.842.921-1.263 2.228-1.263 3.922s.42 2.964 1.263 3.811c.842.847 1.94 1.27 3.292 1.27h12.268v6.107H135.76zm-22.867 0V19.567c0-2.526-1.308-3.789-3.924-3.789h-7.532v15.467H93.86V9.003h15.29c3.728 0 6.54.922 8.434 2.764 1.894 1.842 2.841 4.457 2.841 7.844v11.634h-7.532zM67.293 39v-6.418h2.39c.963 0 1.715-.193 2.256-.58a4.58 4.58 0 001.308-1.426L62.422 9.003h7.623l6.675 14.263 6.855-14.263h7.668L78.749 33.43c-.48.95-.984 1.775-1.51 2.473a7.85 7.85 0 01-1.782 1.739 7.11 7.11 0 01-2.233 1.025c-.827.223-1.781.334-2.864.334h-3.067zm-14.207-7.755l-7.713-9.316a4.647 4.647 0 01-.54-.914 2.306 2.306 0 01-.181-.913v-.535c0-.654.225-1.278.676-1.872l7.487-8.692h8.48l-9.02 10.787 9.47 11.455h-8.66zm-17.41 0V0h7.532v31.245h-7.532zm-35.315 0v-7.488h21.92c.571 0 1.037-.171 1.398-.513.36-.342.541-.825.541-1.449 0-.624-.18-1.114-.541-1.47-.36-.357-.827-.535-1.398-.535H9.38c-1.353 0-2.608-.216-3.766-.647-1.157-.43-2.15-1.025-2.976-1.782a8.005 8.005 0 01-1.94-2.742C.233 13.55 0 12.361 0 11.054 0 9.746.233 8.55.7 7.466A8.184 8.184 0 012.638 4.68c.826-.773 1.819-1.367 2.976-1.783 1.158-.416 2.413-.624 3.766-.624h22.281v7.444H9.742c-.571 0-1.03.163-1.375.49-.346.327-.52.802-.52 1.426 0 .594.174 1.07.52 1.426.345.357.804.535 1.375.535h12.9c1.383 0 2.646.216 3.788.647a9.097 9.097 0 012.977 1.805 8.038 8.038 0 011.962 2.785c.466 1.085.7 2.266.7 3.544 0 1.337-.234 2.548-.7 3.632a8.267 8.267 0 01-1.962 2.808 8.62 8.62 0 01-2.977 1.806c-1.142.416-2.405.624-3.788.624H.36z"
fill="#57B560"
fillRule="evenodd"
/>
</svg>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Sign up for a new account</h2>
<p className="mt-2 text-center text-sm text-gray-600 max-w">
or{" "}
<Link href="/auth/login">
<a className="font-medium text-green-600 hover:text-green-500">sign in</a>
</Link>{" "}
if you already have one
</p>
</div>
<SelfServiceForm
fieldsConfig={fieldsConfig}
validationSchema={validationSchema}
onSubmit={onSubmit}
button="Sign up"
/>
</div>
);
}

View File

@ -1,52 +0,0 @@
import dayjs from "dayjs";
import prettyBytes from "pretty-bytes";
import { useState } from "react";
import Layout from "../components/Layout";
import Table from "../components/Table";
import { SkynetClient } from "skynet-js";
import useAccountsApi from "../services/useAccountsApi";
const skynetClient = new SkynetClient(`https://${process.env.NEXT_PUBLIC_PORTAL_DOMAIN}`);
const getSkylinkLink = ({ skylink }) => skynetClient.getSkylinkUrl(skylink);
const getRelativeDate = ({ downloadedOn }) => dayjs(downloadedOn).format("YYYY-MM-DD HH:mm:ss");
const headers = [
{
key: "name",
name: "File",
formatter: ({ name, skylink }) => (
<>
<p>
<a
href={getSkylinkLink({ skylink })}
className="text-green-600 hover:text-green-900 break-all"
target="_blank"
rel="noopener noreferrer"
>
{name}
</a>
</p>
<p className="text-gray-500 text-xs">{skylink}</p>
</>
),
},
{ key: "size", name: "Size", formatter: ({ size }) => prettyBytes(size) },
{ key: "downloadedOn", name: "Accessed on", formatter: getRelativeDate },
];
const actions = [];
export default function Downloads() {
const [offset, setOffset] = useState(0);
const { data } = useAccountsApi(`user/downloads?pageSize=10&offset=${offset}`, {
revalidateOnMount: true,
});
// preload next page if it exists (based on the response from the current page query)
const nextPageOffset = data && data.offset + data.pageSize < data.count ? data.offset + data.pageSize : offset;
useAccountsApi(`user/downloads?pageSize=10&offset=${nextPageOffset}`);
return (
<Layout title="Your downloads">
<Table {...data} headers={headers} actions={actions} setOffset={setOffset} />
</Layout>
);
}

View File

@ -1,275 +0,0 @@
import copy from "copy-text-to-clipboard";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import prettyBytes from "pretty-bytes";
import Link from "next/link";
import Layout from "../components/Layout";
import { SkynetClient } from "skynet-js";
import config from "../config";
import useAccountsApi from "../services/useAccountsApi";
import React from "react";
dayjs.extend(relativeTime);
const skynetClient = new SkynetClient(`https://${process.env.NEXT_PUBLIC_PORTAL_DOMAIN}`);
function SkylinkList({ items = [], timestamp }) {
return (
<ul className="divide-y divide-gray-200">
{items.slice(0, 3).map((item) => (
<li key={item.id}>
<div className="px-4 py-4 sm:px-6">
<div className="flex items-center justify-between">
<a
href={skynetClient.getSkylinkUrl(item.skylink)}
className="text-sm font-medium text-green-600 hover:text-green-900 truncate"
target="_blank"
rel="noopener noreferrer"
>
{item.name || "— file name not available —"}
</a>
<abbr
className="text-xs text-gray-400 whitespace-nowrap ml-2 cursor-pointer"
title="Click to copy"
onClick={() => copy(`sia://${item.skylink}`)}
>
sia://{item.skylink.substr(0, 5)}…{item.skylink.substr(-5)}
</abbr>
</div>
<div className="mt-2 sm:flex sm:justify-between">
<div className="sm:flex">
<p className="flex items-center text-sm text-gray-500">
<svg
className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"
/>
</svg>
{prettyBytes(item.size)}
</p>
</div>
<div className="mt-2 flex items-center text-sm text-gray-500 sm:mt-0">
<svg
className="flex-shrink-0 mr-1.5 h-4 w-4 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{item[timestamp] && <time dateTime={item[timestamp]}>{dayjs(item[timestamp]).fromNow()}</time>}
</div>
</div>
</div>
</li>
))}
{!items.length && (
<li>
<div className="px-4 py-4 sm:px-6">
<p className="text-sm text-gray-500">no entries yet</p>
</div>
</li>
)}
</ul>
);
}
export default function Home() {
const { data: prices } = useAccountsApi("stripe/prices");
const { data: user } = useAccountsApi("user");
const { data: stats } = useAccountsApi("user/stats");
const { data: downloads } = useAccountsApi("user/downloads?pageSize=3&offset=0");
const { data: uploads } = useAccountsApi("user/uploads?pageSize=3&offset=0");
const [plans, setPlans] = React.useState([config.tiers.starter]);
React.useEffect(() => {
if (prices) setPlans((plans) => [...plans, ...prices].sort((a, b) => a.tier - b.tier));
}, [setPlans, prices]);
const activePlan = plans.find(({ tier }) => user && user.tier === tier);
return (
<Layout title="Dashboard">
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6">
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-2">
<div className="flex flex-col bg-white overflow-hidden shadow rounded-lg">
<div className="flex-grow px-4 py-5 sm:p-6">
<div className="flex items-center">
<div className="flex-shrink-0 bg-green-500 rounded-md p-3">
{/* Heroicon name: outline/users */}
<svg
className="h-6 w-6 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2"
/>
</svg>
</div>
<div className="ml-5 w-0 flex-1">
<dt className="text-sm font-medium text-gray-500 truncate">Current plan</dt>
<dd className="flex items-baseline">
<div className="text-2xl font-semibold text-gray-900">{activePlan?.name}</div>
</dd>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-4 sm:px-6">
<div className="text-sm">
<Link href="/payments">
<a className="font-medium text-green-600 hover:text-green-500 flex items-center">
Check upgrade options
<svg
className="h-4 w-4 ml-1"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"
/>
</svg>
<svg
className="h-4 w-4 ml-1"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"
/>
</svg>
</a>
</Link>
</div>
</div>
</div>
<div className="flex flex-col bg-white overflow-hidden shadow rounded-lg">
<div className="flex-grow px-4 py-5 sm:p-6">
<div className="flex items-center">
<div className="flex-shrink-0 bg-green-500 rounded-md p-3">
<svg
className="h-6 w-6 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"
/>
</svg>
</div>
<div className="ml-5 w-0 flex-1">
<dt className="text-sm font-medium text-gray-500 truncate">Storage used</dt>
<dd className="flex items-baseline">
<div className="text-2xl font-semibold text-grey-900">
{prettyBytes(stats?.totalUploadsSize ?? 0)}
</div>
</dd>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-4 sm:px-6">
<div className="text-sm">
<Link href="/uploads">
<a className="font-medium text-green-600 hover:text-green-500">View all uploads</a>
</Link>
</div>
</div>
</div>
{/* <div className="flex flex-col bg-white overflow-hidden shadow rounded-lg">
<div className="flex-grow px-4 py-5 sm:p-6">
<div className="flex items-center">
<div className="flex-shrink-0 bg-green-500 rounded-md p-3">
<svg
className="h-6 w-6 text-white transform rotate-45"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"
/>
</svg>
</div>
<div className="ml-5 w-0 flex-1">
<dt className="text-sm font-medium text-gray-500 truncate">Bandwidth used</dt>
<dd className="flex items-baseline">
<div className="text-2xl font-semibold text-grey-900">{prettyBytes(stats?.bwDownloads ?? 0)}</div>
</dd>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-4 sm:px-6">
<div className="text-sm">
<Link href="/downloads">
<a className="font-medium text-green-600 hover:text-green-500">View all downloads</a>
</Link>
</div>
</div>
</div> */}
</dl>
{/* ============ */}
<div className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2">
<div className="flex flex-col">
<h3 className="pb-5 text-lg leading-6 font-medium text-gray-900">Recent downloads</h3>
{/* This example requires Tailwind CSS v2.0+ */}
<div className="bg-white shadow overflow-hidden sm:rounded-md">
<SkylinkList items={downloads?.items} timestamp="downloadedOn" />
</div>
</div>
<div className="flex flex-col">
<h3 className="pb-5 text-lg leading-6 font-medium text-gray-900">Recent uploads</h3>
{/* This example requires Tailwind CSS v2.0+ */}
<div className="bg-white shadow overflow-hidden sm:rounded-md">
<SkylinkList items={uploads?.items} timestamp="uploadedOn" />
</div>
</div>
</div>
</div>
</Layout>
);
}

View File

@ -1,44 +0,0 @@
export default function Maintenance() {
return (
<div className="maintenance-gradient">
<div className="container mx-auto">
<section className="flex flex-wrap lg:flex-nowrap justify-center h-screen relative">
<div className="my-auto font-poppins text-palette-600 text-center lg:text-left">
<h1 className="font-bold text-5xl pb-5">
Skynet Accounts <br /> is down for maintenance
</h1>
<p className="lg:w-5/6 pb-1">we are upgrading the service and should be back soon</p>
<p className="lg:w-5/6">you can always contact us at hello@siasky.net</p>
<a
className="block lg:w-5/6 pt-5 text-xs hover:underline cursor-pointer text-palette-500 hover:text-palette-600"
href="https://siasky.net"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 32 32"
className="inline-block transform rotate-180 fill-current"
>
<path
fillRule="evenodd"
d="M17.4969298,9.4969297 L17.4960469,19.1129297 L20.7869064,15.797325 L22.2069531,17.2056813 L17.2526515,22.2011078 C16.8937661,22.5629722 16.3268563,22.5931492 15.9333017,22.289983 L15.8387453,22.2072458 L10.7930469,17.2072458 L12.2008126,15.7866136 L15.4960469,19.0519297 L15.4969298,9.4969297 L17.4969298,9.4969297 Z"
transform="rotate(-90 16.5 15.997)"
/>
</svg>
go back to siasky.net
</a>
</div>
<div className="my-auto text-center w-3/4 lg:w-2/5">
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" className="animate-wiggle">
<title>Skynet</title>
<path d="m-.0004 6.4602 21.3893 11.297c.561.2935.6633 1.0532.1999 1.4846h-.011a10.0399 10.0399 0 0 1-2.2335 1.5307c-6.912 3.4734-14.9917-1.838-14.5438-9.5605l2.8601 1.9752c.856 4.508 5.6187 7.1094 9.8742 5.3932zm8.6477 3.1509 14.3661 5.6785a.8704.8704 0 0 1 .5197 1.0466v.0182c-.1537.5377-.7668.7938-1.2575.5252zm5.2896-7.4375c2.7093-.2325 6.0946.7869 8.1116 3.3871 1.699 2.1951 2.0497 4.8772 1.9298 7.6465v-.007c-.0478.5874-.6494.9616-1.1975.745l-9.7652-3.8596 9.0656 2.4313a7.296 7.296 0 0 0-1.0677-4.5631c-2.9683-4.7678-9.9847-4.5344-12.6297.4201a7.5048 7.5048 0 0 0-.398.8831L5.5546 7.9614c.069-.1017.1417-.198.2144-.2962.1163-.2416.2417-.487.3798-.7268 1.6118-2.7911 4.3102-4.4338 7.1558-4.6973.2108-.0182.4215-.049.6323-.0672z" />
</svg>
</div>
</section>
</div>
</div>
);
}

View File

@ -1,187 +0,0 @@
import dayjs from "dayjs";
import Layout from "../components/Layout";
import ky from "ky";
import * as React from "react";
import classnames from "classnames";
import prettyBytes from "pretty-bytes";
import config from "../config";
import useAccountsApi from "../services/useAccountsApi";
import { isFreeTier, isPaidTier } from "../services/tiers";
const ActiveBadge = () => {
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs bg-green-200 text-green-800 ml-3">
active
</span>
);
};
export default function Payments() {
const { data: user } = useAccountsApi("user");
const { data: stats } = useAccountsApi("user/stats");
const { data: prices } = useAccountsApi("stripe/prices");
const [plans, setPlans] = React.useState([config.tiers.starter]);
const [selectedPlan, setSelectedPlan] = React.useState(null);
React.useEffect(() => {
if (prices) setPlans((plans) => [...plans, ...prices].sort((a, b) => a.tier - b.tier));
}, [setPlans, prices]);
const activePlan = plans.find(({ tier }) => user && user.tier === tier);
React.useEffect(() => {
setSelectedPlan(activePlan);
}, [activePlan, setSelectedPlan]);
const handleSubscribe = async () => {
try {
const price = selectedPlan.stripe;
const { sessionId } = await ky.post("/api/stripe/checkout", { json: { price } }).json();
const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
await stripe.redirectToCheckout({ sessionId });
} catch (error) {
console.log(error); // todo: handle error
}
};
return (
<Layout title="Payments">
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6">
<div className="space-y-6 sm:px-6 lg:px-0 lg:col-span-9">
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<dt className="text-sm font-medium text-gray-500 truncate">Current plan</dt>
<dd className="mt-1 text-3xl font-semibold text-gray-900">{activePlan?.name || "—"}</dd>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<dt className="text-sm font-medium text-gray-500 truncate">Subscription status</dt>
<dd className="mt-1 text-3xl font-semibold text-gray-900 capitalize">
{isFreeTier(activePlan?.tier) ? "—" : user?.subscriptionStatus}
</dd>
</div>
{user?.subscriptionCancelAtPeriodEnd && (
<div className="bg-gray-50 px-4 py-4 sm:px-6">
<div className="text-sm">
Your plan will be cancelled on {dayjs(user.subscriptionCancelAt).format("D MMM YYYY")}.
</div>
</div>
)}
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<dt className="text-sm font-medium text-gray-500 truncate">Storage used</dt>
<dd className="mt-1 text-3xl font-semibold text-gray-900">
{prettyBytes(stats?.totalUploadsSize ?? 0)}
</dd>
</div>
</div>
</dl>
<section aria-labelledby="plan_heading">
<form action="#" method="POST">
<div className="shadow sm:rounded-md sm:overflow-hidden">
<div className="bg-white py-6 px-4 space-y-6 sm:p-6">
<div className="-ml-4 -mt-2 flex items-center justify-between flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-2">
<h3 id="plan_heading" className="text-lg leading-6 font-medium text-gray-900">
Plan
</h3>
</div>
</div>
<fieldset>
<legend className="sr-only">Pricing plans</legend>
<ul className="relative bg-white rounded-md -space-y-px">
{plans.map((plan, index) => (
<li key={plan.id}>
<label
className={`${classnames({
"rounded-tl-md rounded-tr-md": index === 0,
"rounded-bl-md rounded-br-md": index === plans.length - 1,
"bg-green-50 border-green-200 z-10": plan === selectedPlan,
"border-gray-200": plan !== selectedPlan,
"cursor-pointer": isFreeTier(user?.tier),
})} relative border p-4 flex flex-col md:pl-4 md:pr-6 md:grid md:grid-cols-3`}
>
<span className="flex items-center text-sm">
{isFreeTier(activePlan?.tier) && (
<input
name="pricing_plan"
type="radio"
className="h-4 w-4 text-orange-500 focus:ring-gray-900 border-gray-300"
aria-describedby="plan-option-pricing-0 plan-option-limit-0"
checked={plan === selectedPlan}
onChange={() => setSelectedPlan(plan)}
/>
)}
<span
className={classnames("ml-3 font-medium", {
"text-green-900": plan === selectedPlan,
"text-gray-900": plan !== selectedPlan,
})}
>
{plan.name}
</span>
{activePlan === plan && <ActiveBadge />}
</span>
<p id="plan-option-pricing-0" className="ml-6 pl-1 text-sm md:ml-0 md:pl-0 md:text-center">
<span
className={classnames("font-medium", {
"text-green-900": plan === selectedPlan,
"text-gray-900": plan !== selectedPlan,
})}
>
{plan.price ? `$${plan.price} / mo` : "no cost"}
</span>
</p>
<p
className={classnames("ml-6 pl-1 text-sm md:ml-0 md:pl-0 md:text-right", {
"text-green-700": plan === selectedPlan,
"text-gray-500": plan !== selectedPlan,
})}
>
{plan.description}
</p>
</label>
</li>
))}
</ul>
</fieldset>
</div>
<div className="px-4 py-3 bg-gray-50 sm:px-6 flex flex-col">
{user && isPaidTier(user.tier) ? (
<div className="text-sm text-gray-500 flex justify-between items-center space-x-4 md:space-x-0 flex-col md:flex-row space-y-4 md:space-y-0">
<span className="text-center md:text-left">
Use Stripe Customer Portal to manage your active subscription, payment methods and view your
billing history
</span>
<a
href="/api/stripe/billing"
className="text-right flex-shrink-0 w-full md:w-auto bg-green-800 disabled:bg-gray-300 disabled:text-gray-400 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-green-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900"
>
Stripe Customer Portal
</a>
</div>
) : (
<button
type="button"
onClick={handleSubscribe}
disabled={activePlan === selectedPlan}
className="self-end text-right w-full md:w-auto bg-green-800 disabled:bg-gray-300 disabled:text-gray-400 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-green-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900"
>
Subscribe
</button>
)}
</div>
</div>
</form>
</section>
</div>
</div>
</Layout>
);
}

View File

@ -1,389 +0,0 @@
import Layout from "../components/Layout";
export default function Payments() {
return (
<Layout title="Pricing Plans">
{/* This example requires Tailwind CSS v2.0+ */}
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="sm:flex sm:flex-col sm:align-center">
<p className="mt-5 text-xl text-gray-500 sm:text-center">
Start using for free, then add a plan to improve the experience. Account plans unlock additional features.
</p>
<div className="relative self-center mt-6 bg-gray-100 rounded-lg p-0.5 flex sm:mt-8">
<button
type="button"
className="relative w-1/2 bg-white border-gray-200 rounded-md shadow-sm py-2 text-sm font-medium text-gray-700 whitespace-nowrap focus:outline-none focus:ring-2 focus:ring-green-500 focus:z-10 sm:w-auto sm:px-8"
>
Monthly billing
</button>
<button
type="button"
className="ml-0.5 relative w-1/2 border border-transparent rounded-md py-2 text-sm font-medium text-gray-700 whitespace-nowrap focus:outline-none focus:ring-2 focus:ring-green-500 focus:z-10 sm:w-auto sm:px-8"
>
Yearly billing
</button>
</div>
</div>
<div className="mt-12 space-y-4 sm:mt-16 sm:space-y-0 sm:grid sm:grid-cols-2 sm:gap-6 lg:max-w-4xl lg:mx-auto xl:max-w-none xl:mx-0 xl:grid-cols-4">
<div className="border border-gray-200 rounded-lg shadow-sm divide-y divide-gray-200">
<div className="p-6">
<h2 className="text-lg leading-6 font-medium text-gray-900">Free</h2>
<p className="mt-4 text-sm text-gray-500">All the basics for starting a new business</p>
<p className="mt-8">
<span className="text-4xl font-extrabold text-gray-900">no cost</span>
</p>
<a
href="#"
className="opacity-50 mt-8 block w-full bg-gray-400 border border-gray-400 rounded-md py-2 text-sm font-semibold text-white text-center hover:bg-gray-900"
>
Active
</a>
</div>
<div className="pt-6 pb-8 px-6">
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What&apos;s included</h3>
<ul className="mt-6 space-y-4">
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Potenti felis, in cras at at ligula nunc.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Orci neque eget pellentesque.</span>
</li>
</ul>
</div>
</div>
<div className="border border-gray-200 rounded-lg shadow-sm divide-y divide-gray-200">
<div className="p-6">
<h2 className="text-lg leading-6 font-medium text-gray-900">Skynet Plus</h2>
<p className="mt-4 text-sm text-gray-500">All the basics for starting a new business</p>
<p className="mt-8">
<span className="text-4xl font-extrabold text-gray-900">$5</span>
<span className="text-base font-medium text-gray-500">/mo</span>
</p>
<a
href="#"
className="mt-8 block w-full bg-gray-800 border border-gray-800 rounded-md py-2 text-sm font-semibold text-white text-center hover:bg-gray-900"
>
Buy Skynet Plus
</a>
</div>
<div className="pt-6 pb-8 px-6">
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What&apos;s included</h3>
<ul className="mt-6 space-y-4">
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Potenti felis, in cras at at ligula nunc. </span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Orci neque eget pellentesque.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Donec mauris sit in eu tincidunt etiam.</span>
</li>
</ul>
</div>
</div>
<div className="border border-gray-200 rounded-lg shadow-sm divide-y divide-gray-200">
<div className="p-6">
<h2 className="text-lg leading-6 font-medium text-gray-900">Skynet Pro</h2>
<p className="mt-4 text-sm text-gray-500">All the basics for starting a new business</p>
<p className="mt-8">
<span className="text-4xl font-extrabold text-gray-900">$20</span>
<span className="text-base font-medium text-gray-500">/mo</span>
</p>
<a
href="#"
className="mt-8 block w-full bg-gray-800 border border-gray-800 rounded-md py-2 text-sm font-semibold text-white text-center hover:bg-gray-900"
>
Buy Skynet Pro
</a>
</div>
<div className="pt-6 pb-8 px-6">
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What&apos;s included</h3>
<ul className="mt-6 space-y-4">
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Potenti felis, in cras at at ligula nunc. </span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Orci neque eget pellentesque.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Donec mauris sit in eu tincidunt etiam.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Faucibus volutpat magna.</span>
</li>
</ul>
</div>
</div>
<div className="border border-gray-200 rounded-lg shadow-sm divide-y divide-gray-200">
<div className="p-6">
<h2 className="text-lg leading-6 font-medium text-gray-900">Skynet Extreme</h2>
<p className="mt-4 text-sm text-gray-500">All the basics for starting a new business</p>
<p className="mt-8">
<span className="text-4xl font-extrabold text-gray-900">$80</span>
<span className="text-base font-medium text-gray-500">/mo</span>
</p>
<a
href="#"
className="mt-8 block w-full bg-gray-800 border border-gray-800 rounded-md py-2 text-sm font-semibold text-white text-center hover:bg-gray-900"
>
Buy Skynet Extreme
</a>
</div>
<div className="pt-6 pb-8 px-6">
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What&apos;s included</h3>
<ul className="mt-6 space-y-4">
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Potenti felis, in cras at at ligula nunc. </span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Orci neque eget pellentesque.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Donec mauris sit in eu tincidunt etiam.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Faucibus volutpat magna.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Id sed tellus in varius quisque.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Risus egestas faucibus.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Risus cursus ullamcorper.</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</Layout>
);
}

View File

@ -1,67 +0,0 @@
import dayjs from "dayjs";
import prettyBytes from "pretty-bytes";
import { useState } from "react";
import ky from "ky";
import { toast } from "react-toastify";
import Layout from "../components/Layout";
import Table from "../components/Table";
import { SkynetClient } from "skynet-js";
import useAccountsApi from "../services/useAccountsApi";
const skynetClient = new SkynetClient(`https://${process.env.NEXT_PUBLIC_PORTAL_DOMAIN}`);
const getSkylinkLink = ({ skylink }) => skynetClient.getSkylinkUrl(skylink);
const getRelativeDate = ({ uploadedOn }) => dayjs(uploadedOn).format("YYYY-MM-DD HH:mm:ss");
const headers = [
{
key: "name",
name: "File",
formatter: ({ name, skylink }) => (
<>
<p>
<a
href={getSkylinkLink({ skylink })}
className="text-green-600 hover:text-green-900 break-all"
target="_blank"
rel="noopener noreferrer"
>
{name}
</a>
</p>
<p className="text-gray-500 text-xs">{skylink}</p>
</>
),
},
{ key: "size", name: "Size", formatter: ({ size }) => prettyBytes(size) },
{ key: "uploadedOn", name: "Uploaded on", formatter: getRelativeDate },
];
const actions = [
{
name: "Unpin Skylink",
action: async ({ skylink }, mutate) => {
await toast.promise(ky.delete(`/api/user/uploads/${skylink}`), {
pending: "Unpinning Skylink",
success: "Skylink unpinned",
error: (error) => error.message,
});
mutate();
},
},
];
export default function Uploads() {
const [offset, setOffset] = useState(0);
const { data, mutate } = useAccountsApi(`user/uploads?pageSize=10&offset=${offset}`, {
revalidateOnMount: true,
});
// preload next page if it exists (based on the response from the current page query)
const nextPageOffset = data && data.offset + data.pageSize < data.count ? data.offset + data.pageSize : offset;
useAccountsApi(`user/uploads?pageSize=10&offset=${nextPageOffset}`);
return (
<Layout title="Your uploads">
<Table {...data} mutate={mutate} headers={headers} actions={actions} setOffset={setOffset} />
</Layout>
);
}

View File

@ -1,57 +0,0 @@
import Link from "next/link";
import { useRouter } from "next/router";
import accountsApi from "../../services/accountsApi";
import useAnonRoute from "../../services/useAnonRoute";
import React from "react";
export default function Recover() {
useAnonRoute(); // ensure user is not logged in
const router = useRouter();
const [error, setError] = React.useState("");
const [confirmed, setConfirmed] = React.useState(false);
React.useEffect(() => {
async function confirm() {
try {
await accountsApi.get("user/confirm", { searchParams: { token: router.query.token } });
setConfirmed(true);
setTimeout(() => {
router.push("/");
}, 5000);
} catch (error) {
if (error.response) {
const data = await error.response.json();
setError(data.message ?? error.toString());
} else {
setError(error.toString());
}
}
}
if (router.query.token) confirm();
}, [router, setConfirmed]);
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
{!confirmed && !error && (
<h2 className="mt-6 text-center text-xl text-gray-900">Confirming your email, please wait.</h2>
)}
{confirmed && (
<h2 className="mt-6 text-center text-xl">
Email confirmed! You will be redirected to the{" "}
<Link href="/">
<a className="text-primary hover:text-primary-light">dashboard</a>
</Link>{" "}
in 3 seconds.
</h2>
)}
{error && <h2 className="mt-6 text-center text-xl text-red-900">{error}</h2>}
</div>
</div>
);
}

View File

@ -1,78 +0,0 @@
import * as Yup from "yup";
import { useRouter } from "next/router";
import accountsApi from "../../services/accountsApi";
import useAnonRoute from "../../services/useAnonRoute";
import SelfServiceForm from "../../components/Form/SelfServiceForm";
const fieldsConfig = [
{
name: "password",
type: "password",
label: "Password",
autoComplete: "new-password",
position: 0,
},
{
name: "confirmPassword",
type: "password",
label: "Password repeated",
autoComplete: "new-password",
position: 1,
},
{
name: "token",
type: "hidden",
position: 2,
},
];
const validationSchema = Yup.object().shape({
password: Yup.string().required("Password is required").min(6, "Password has to be at least 6 characters long"),
confirmPassword: Yup.string().oneOf([Yup.ref("password"), null], "Passwords must match"),
});
export default function Recover() {
useAnonRoute(); // ensure user is not logged in
const router = useRouter();
const onSubmit = async (values) => {
await accountsApi.post("user/recover", {
json: {
token: router.query.token,
password: values.password,
confirmPassword: values.confirmPassword,
},
});
router.push("/");
};
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<svg
xmlns="http://www.w3.org/2000/svg"
width="169"
height="39"
viewBox="0 0 169 39"
className="mx-auto h-12 w-auto"
>
<path
d="M160.701 31.245c-2.736 0-4.788-.706-6.156-2.118-1.369-1.411-2.053-3.424-2.053-6.039V2.674h7.487v6.33H169v6.15h-9.02v7.355c0 1.753.886 2.63 2.66 2.63h6.134v6.106h-8.073zm-24.942 0c-1.744 0-3.352-.268-4.826-.803-1.473-.535-2.751-1.292-3.833-2.273a10.275 10.275 0 01-2.526-3.521c-.602-1.367-.902-2.882-.902-4.546 0-1.635.3-3.135.902-4.502a10.275 10.275 0 012.526-3.521c1.082-.98 2.36-1.738 3.833-2.273 1.474-.535 3.082-.803 4.826-.803h5.728c1.293 0 2.42.179 3.383.535.962.357 1.767.847 2.413 1.471a5.823 5.823 0 011.443 2.251c.316.877.474 1.835.474 2.875 0 1.961-.571 3.432-1.714 4.412-1.143.981-3.022 1.471-5.638 1.471h-8.028v-4.056h6.45c1.172 0 1.759-.52 1.759-1.56 0-1.1-.572-1.649-1.714-1.649h-4.6c-1.354 0-2.451.46-3.293 1.382-.842.921-1.263 2.228-1.263 3.922s.42 2.964 1.263 3.811c.842.847 1.94 1.27 3.292 1.27h12.268v6.107H135.76zm-22.867 0V19.567c0-2.526-1.308-3.789-3.924-3.789h-7.532v15.467H93.86V9.003h15.29c3.728 0 6.54.922 8.434 2.764 1.894 1.842 2.841 4.457 2.841 7.844v11.634h-7.532zM67.293 39v-6.418h2.39c.963 0 1.715-.193 2.256-.58a4.58 4.58 0 001.308-1.426L62.422 9.003h7.623l6.675 14.263 6.855-14.263h7.668L78.749 33.43c-.48.95-.984 1.775-1.51 2.473a7.85 7.85 0 01-1.782 1.739 7.11 7.11 0 01-2.233 1.025c-.827.223-1.781.334-2.864.334h-3.067zm-14.207-7.755l-7.713-9.316a4.647 4.647 0 01-.54-.914 2.306 2.306 0 01-.181-.913v-.535c0-.654.225-1.278.676-1.872l7.487-8.692h8.48l-9.02 10.787 9.47 11.455h-8.66zm-17.41 0V0h7.532v31.245h-7.532zm-35.315 0v-7.488h21.92c.571 0 1.037-.171 1.398-.513.36-.342.541-.825.541-1.449 0-.624-.18-1.114-.541-1.47-.36-.357-.827-.535-1.398-.535H9.38c-1.353 0-2.608-.216-3.766-.647-1.157-.43-2.15-1.025-2.976-1.782a8.005 8.005 0 01-1.94-2.742C.233 13.55 0 12.361 0 11.054 0 9.746.233 8.55.7 7.466A8.184 8.184 0 012.638 4.68c.826-.773 1.819-1.367 2.976-1.783 1.158-.416 2.413-.624 3.766-.624h22.281v7.444H9.742c-.571 0-1.03.163-1.375.49-.346.327-.52.802-.52 1.426 0 .594.174 1.07.52 1.426.345.357.804.535 1.375.535h12.9c1.383 0 2.646.216 3.788.647a9.097 9.097 0 012.977 1.805 8.038 8.038 0 011.962 2.785c.466 1.085.7 2.266.7 3.544 0 1.337-.234 2.548-.7 3.632a8.267 8.267 0 01-1.962 2.808 8.62 8.62 0 01-2.977 1.806c-1.142.416-2.405.624-3.788.624H.36z"
fill="#57B560"
fillRule="evenodd"
/>
</svg>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Set new password</h2>
</div>
<SelfServiceForm
fieldsConfig={fieldsConfig}
validationSchema={validationSchema}
onSubmit={onSubmit}
button="Confirm"
/>
</div>
);
}

View File

@ -1,85 +0,0 @@
import * as Yup from "yup";
import Layout from "../../components/Layout";
import SelfServiceForm from "../../components/Form/SelfServiceForm";
import accountsApi from "../../services/accountsApi";
const profileConfig = [
{
name: "email",
type: "text",
label: "New email address",
autoComplete: "email",
position: 0,
},
{
name: "confirmEmail",
type: "text",
label: "New email address repeated",
autoComplete: "email",
position: 1,
},
];
const passwordConfig = [
{
name: "password",
type: "password",
label: "New password",
position: 1,
},
{
name: "confirmPassword",
type: "password",
label: "New password repeated",
position: 2,
},
];
const emailValidationSchema = Yup.object().shape({
email: Yup.string().required("Email is required").email("This email is invalid"),
confirmEmail: Yup.string().oneOf([Yup.ref("email"), null], "Emails must match"),
});
const passwordValidationSchema = Yup.object().shape({
password: Yup.string().required("Password is required").min(6, "Password has to be at least 6 characters long"),
confirmPassword: Yup.string().oneOf([Yup.ref("password"), null], "Passwords must match"),
});
export default function Settings() {
const onEmailSubmit = async (values) => {
await accountsApi.put("user", {
json: {
email: values.email,
},
});
};
const onPasswordSubmit = async (values) => {
await accountsApi.put("user", {
json: {
password: values.password,
},
});
};
return (
<Layout title="Settings">
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6 grid grid-cols-1 gap-5 sm:grid-cols-2">
<SelfServiceForm
fieldsConfig={profileConfig}
validationSchema={emailValidationSchema}
onSubmit={onEmailSubmit}
title="Account settings"
button="Update"
/>
<SelfServiceForm
fieldsConfig={passwordConfig}
validationSchema={passwordValidationSchema}
onSubmit={onPasswordSubmit}
title="Authentication settings"
button="Update"
/>
</div>
</Layout>
);
}

View File

@ -1,3 +0,0 @@
import ky from "ky";
export default ky.create({ prefixUrl: "/api" });

View File

@ -1,65 +0,0 @@
// @source https://github.com/trekhleb/javascript-algorithms/blob/master/src/algorithms/sets/longest-common-subsequence/longestCommonSubsequence.js
// @license MIT https://github.com/trekhleb/javascript-algorithms/blob/master/LICENSE
/**
* @param {string[]} set1
* @param {string[]} set2
* @return {string[]}
*/
export default function longestCommonSubsequence(set1, set2) {
// Init LCS matrix.
const lcsMatrix = Array(set2.length + 1)
.fill(null)
.map(() => Array(set1.length + 1).fill(null));
// Fill first row with zeros.
for (let columnIndex = 0; columnIndex <= set1.length; columnIndex += 1) {
lcsMatrix[0][columnIndex] = 0;
}
// Fill first column with zeros.
for (let rowIndex = 0; rowIndex <= set2.length; rowIndex += 1) {
lcsMatrix[rowIndex][0] = 0;
}
// Fill rest of the column that correspond to each of two strings.
for (let rowIndex = 1; rowIndex <= set2.length; rowIndex += 1) {
for (let columnIndex = 1; columnIndex <= set1.length; columnIndex += 1) {
if (set1[columnIndex - 1] === set2[rowIndex - 1]) {
lcsMatrix[rowIndex][columnIndex] = lcsMatrix[rowIndex - 1][columnIndex - 1] + 1;
} else {
lcsMatrix[rowIndex][columnIndex] = Math.max(
lcsMatrix[rowIndex - 1][columnIndex],
lcsMatrix[rowIndex][columnIndex - 1]
);
}
}
}
// Calculate LCS based on LCS matrix.
if (!lcsMatrix[set2.length][set1.length]) {
// If the length of largest common string is zero then return empty string.
return [""];
}
const longestSequence = [];
let columnIndex = set1.length;
let rowIndex = set2.length;
while (columnIndex > 0 || rowIndex > 0) {
if (set1[columnIndex - 1] === set2[rowIndex - 1]) {
// Move by diagonal left-top.
longestSequence.unshift(set1[columnIndex - 1]);
columnIndex -= 1;
rowIndex -= 1;
} else if (lcsMatrix[rowIndex][columnIndex] === lcsMatrix[rowIndex][columnIndex - 1]) {
// Move left.
columnIndex -= 1;
} else {
// Move up.
rowIndex -= 1;
}
}
return longestSequence;
}

View File

@ -1,2 +0,0 @@
export const isFreeTier = (tier) => tier === 1;
export const isPaidTier = (tier) => tier > 1;

View File

@ -1,19 +0,0 @@
import useSWR from "swr";
import { useRouter } from "next/router";
import { StatusCodes } from "http-status-codes";
const fetcher = (url, router) => {
return fetch(url).then((res) => {
if (res.status === StatusCodes.UNAUTHORIZED) {
router.push(`/auth/login?return_to=${encodeURIComponent(window.location.href)}`);
}
return res.json();
});
};
export default function useAccountsApi(key, config) {
const router = useRouter();
return useSWR(`/api/${key}`, (url) => fetcher(url, router), config);
}

View File

@ -1,15 +0,0 @@
import useSWR from "swr";
import { useRouter } from "next/router";
import { StatusCodes } from "http-status-codes";
const fetcher = (url, router) => {
return fetch(url).then((res) => {
if (res.status === StatusCodes.OK) router.push("/");
});
};
export default function useAnonRoute() {
const router = useRouter();
return useSWR("/api/user", (url) => fetcher(url, router));
}

View File

@ -1,9 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.maintenance-gradient {
background: #00c65e; /* fallback for old browsers */
background: -webkit-linear-gradient(to right, #33d17e, #00c65e); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(to right, #33d17e, #00c65e);
}

View File

@ -1,64 +0,0 @@
const defaultTheme = require("tailwindcss/defaultTheme");
const plugin = require("tailwindcss/plugin");
const colors = {
primary: { light: "#33D17E", DEFAULT: "#00c65e" },
warning: "#ffd567",
error: "#ED5454",
palette: {
100: "#f5f7f7",
200: "#d4dddb",
300: "#9e9e9e",
400: "#555555",
500: "#242424",
600: "#0d0d0d",
},
};
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
screens: {
sm: "640px",
tablet: "640px",
md: "768px",
lg: "1024px",
desktop: "1024px",
xl: "1280px",
hires: "1408px",
"2xl": "1536px",
},
backgroundColor: (theme) => ({ ...theme("colors"), ...colors }),
borderColor: (theme) => ({ ...theme("colors"), ...colors }),
textColor: (theme) => ({ ...theme("colors"), ...colors }),
placeholderColor: (theme) => ({ ...theme("colors"), ...colors }),
extend: {
fontFamily: {
sans: ["Sora", ...defaultTheme.fontFamily.sans],
content: ["Source\\ Sans\\ Pro", ...defaultTheme.fontFamily.sans],
},
backgroundColor: ["disabled"],
textColor: ["disabled"],
keyframes: {
wiggle: {
"0%, 100%": { transform: "rotate(-3deg)" },
"50%": { transform: "rotate(3deg)" },
},
},
animation: {
wiggle: "wiggle 3s ease-in-out infinite",
},
},
},
plugins: [
require("@tailwindcss/typography"),
require("@tailwindcss/forms"),
plugin(function ({ addBase, theme }) {
addBase({
body: {
color: theme("textColor.palette.600"),
},
});
}),
],
};

File diff suppressed because it is too large Load Diff