custom frontend

This commit is contained in:
Karol Wypchlo 2021-02-03 15:06:44 +01:00
parent 783b4a78e6
commit 54dad9b187
25 changed files with 1529 additions and 5 deletions

View File

@ -128,6 +128,28 @@ services:
shared: shared:
ipv4_address: 10.10.10.82 ipv4_address: 10.10.10.82
dashboard:
build:
context: ./packages/dashboard
dockerfile: Dockerfile
container_name: dashboard
restart: unless-stopped
logging: *default-logging
environment:
- SECURITY_MODE=jwks
- PROJECT_NAME=Skynet
- BASE_URL=/
- KRATOS_BROWSER_URL=/.ory/kratos/public
- JWKS_URL=http://oathkeeper:4456/.well-known/jwks.json
- KRATOS_PUBLIC_URL=http://kratos:4433/
- KRATOS_ADMIN_URL=http://kratos:4434/
- SQA_OPT_OUT=true
networks:
shared:
ipv4_address: 10.10.10.83
expose:
- 3000
oathkeeper: oathkeeper:
image: oryd/oathkeeper:v0.38 image: oryd/oathkeeper:v0.38
container_name: oathkeeper container_name: oathkeeper

View File

@ -18,10 +18,10 @@
mutators: mutators:
- handler: noop - handler: noop
- id: "ory:kratos-selfservice-ui-node:anonymous" - id: "dashboard:anonymous"
upstream: upstream:
preserve_host: true preserve_host: true
url: "http://kratos-selfservice-ui-node:4435" url: "http://dashboard:3000"
match: match:
url: "http://oathkeeper:4455/<{error,recovery,verify,auth/*,**.css,**.js}{/,}>" url: "http://oathkeeper:4455/<{error,recovery,verify,auth/*,**.css,**.js}{/,}>"
methods: methods:
@ -33,12 +33,12 @@
mutators: mutators:
- handler: noop - handler: noop
- id: "ory:kratos-selfservice-ui-node:protected" - id: "dashboard:protected"
upstream: upstream:
preserve_host: true preserve_host: true
url: "http://kratos-selfservice-ui-node:4435" url: "http://dashboard:3000"
match: match:
url: "http://oathkeeper:4455/<{,debug,dashboard,settings}>" url: "http://oathkeeper:4455/<{,uploads,downloads,payments}>"
methods: methods:
- GET - GET
authenticators: authenticators:

35
packages/dashboard/.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# 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*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
.next

View File

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

View File

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

View File

@ -0,0 +1,15 @@
FROM node:14.15.0-alpine
WORKDIR /usr/app
COPY pages ./pages
COPY public ./public
COPY src ./src
COPY styles ./styles
COPY package.json .
COPY postcss.config.js .
COPY tailwind.config.js .
ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn --no-lockfile && yarn build
CMD ["yarn", "start"]

View File

@ -0,0 +1,34 @@
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
npm run dev
# or
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

@ -0,0 +1,21 @@
{
"name": "dashboard",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@ory/kratos-client": "^0.5.4-alpha.1",
"autoprefixer": "^10.2.4",
"next": "^10.0.6",
"postcss": "^8.2.4",
"prettier": "^2.2.1",
"react": "17.0.1",
"react-dom": "17.0.1",
"swr": "^0.4.1",
"tailwindcss": "^2.0.2"
}
}

View File

@ -0,0 +1,7 @@
import "tailwindcss/tailwind.css";
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;

View File

@ -0,0 +1,6 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
export default (req, res) => {
res.statusCode = 200;
res.json({ name: "John Doe" });
};

View File

@ -0,0 +1,148 @@
import Link from "next/link";
import { Configuration, PublicApi } from "@ory/kratos-client";
import config from "../../src/config";
const kratos = new PublicApi(new Configuration({ basePath: config.kratos.public }));
export async function getServerSideProps(context) {
const flow = context.query.flow;
// The flow is used to identify the login and registration flow and
// return data like the csrf_token and so on.
if (!flow || typeof flow !== "string") {
console.log("No flow ID found in URL, initializing login flow.");
return {
redirect: {
permanent: false,
destination: `${config.kratos.browser}/self-service/login/browser`,
},
};
}
try {
const { status, data } = await kratos.getSelfServiceLoginFlow(flow);
if (status === 200) return { props: { flow: data } };
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
} catch (error) {
return {
redirect: {
permanent: false,
destination: `${config.kratos.browser}/self-service/login/browser`,
},
};
}
}
const fieldProps = {
identifier: {
label: "Email address",
autoComplete: "email",
position: 0,
},
password: {
label: "Password",
autoComplete: "current-password",
position: 1,
},
csrf_token: {
position: 99,
},
};
export default function Login({ flow }) {
const fields = flow.methods.password.config.fields
.map((field) => ({
...field,
...fieldProps[field.name],
}))
.sort((a, b) => (a.position < b.position ? -1 : 1));
console.log(flow);
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't have one yet
</p>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form
className="space-y-6"
action={flow.methods.password.config.action}
method={flow.methods.password.config.method}
>
{fields.map((field) => (
<div key={field.name}>
{field.type !== "hidden" && (
<label htmlFor={field.name} className="block text-sm font-medium text-gray-700 mb-1">
{fieldProps[field.name].label}
</label>
)}
<div>
<input
id={field.name}
name={field.name}
type={field.type}
autoComplete={fieldProps[field.name]}
required={field.required}
value={field.value || undefined}
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm"
/>
</div>
</div>
))}
<div className="flex items-center justify-between">
{/* <div className="flex items-center">
<input
id="remember_me"
name="remember_me"
type="checkbox"
className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
/>
<label htmlFor="remember_me" className="ml-2 block text-sm text-gray-900">
Remember me
</label>
</div> */}
<div className="text-sm">
<Link href="/auth/recovery">
<a className="font-medium text-green-600 hover:text-green-500">Forgot your password?</a>
</Link>
</div>
</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"
>
Sign in
</button>
</div>
</form>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,120 @@
import Link from "next/link";
import { Configuration, PublicApi } from "@ory/kratos-client";
import config from "../../src/config";
const kratos = new PublicApi(new Configuration({ basePath: config.kratos.public }));
export async function getServerSideProps(context) {
const flow = context.query.flow;
// The flow is used to identify the login and registration flow and
// return data like the csrf_token and so on.
if (!flow || typeof flow !== "string") {
console.log("No flow ID found in URL, initializing recovery flow.");
return {
redirect: {
permanent: false,
destination: `${config.kratos.browser}/self-service/recovery/browser`,
},
};
}
try {
const { status, data } = await kratos.getSelfServiceRecoveryFlow(flow);
if (status === 200) return { props: { flow: data } };
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
} catch (error) {
return {
redirect: {
permanent: false,
destination: `${config.kratos.browser}/self-service/recovery/browser`,
},
};
}
}
const fieldProps = {
email: {
label: "Your email",
autoComplete: "email",
position: 0,
},
csrf_token: {
position: 99,
},
};
export default function Registration({ flow }) {
const fields = flow.methods.link.config.fields
.map((field) => ({
...field,
...fieldProps[field.name],
}))
.sort((a, b) => (a.position < b.position ? -1 : 1));
console.log(flow);
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">
or{" "}
<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>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form className="space-y-6" action={flow.methods.link.config.action} method={flow.methods.link.config.method}>
{fields.map((field) => (
<div key={field.name}>
{field.type !== "hidden" && (
<label htmlFor={field.name} className="block text-sm font-medium text-gray-700 mb-1">
{fieldProps[field.name]?.label ?? field.name}
</label>
)}
<div>
<input
id={field.name}
name={field.name}
type={field.type}
autoComplete={fieldProps[field.name]}
required={field.required}
value={field.value || undefined}
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm"
/>
</div>
</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"
>
Send recovery link
</button>
</div>
</form>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,140 @@
import Link from "next/link";
import { Configuration, PublicApi } from "@ory/kratos-client";
import config from "../../src/config";
const kratos = new PublicApi(new Configuration({ basePath: config.kratos.public }));
export async function getServerSideProps(context) {
const flow = context.query.flow;
// The flow is used to identify the login and registration flow and
// return data like the csrf_token and so on.
if (!flow || typeof flow !== "string") {
console.log("No flow ID found in URL, initializing registration flow.");
return {
redirect: {
permanent: false,
destination: `${config.kratos.browser}/self-service/registration/browser`,
},
};
}
try {
const { status, data } = await kratos.getSelfServiceRegistrationFlow(flow);
if (status === 200) return { props: { flow: data } };
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
} catch (error) {
return {
redirect: {
permanent: false,
destination: `${config.kratos.browser}/self-service/registration/browser`,
},
};
}
}
const fieldProps = {
"traits.email": {
label: "Email address",
autoComplete: "email",
position: 0,
},
"traits.name.first": {
label: "First name",
autoComplete: "given-name",
position: 1,
},
"traits.name.last": {
label: "Last name",
autoComplete: "family-name",
position: 2,
},
password: {
label: "Password",
autoComplete: "new-password",
position: 4,
},
csrf_token: {
position: 99,
},
};
export default function Registration({ flow }) {
const fields = flow.methods.password.config.fields
.map((field) => ({
...field,
...fieldProps[field.name],
}))
.sort((a, b) => (a.position < b.position ? -1 : 1));
console.log(flow);
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>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form
className="space-y-6"
action={flow.methods.password.config.action}
method={flow.methods.password.config.method}
>
{fields.map((field) => (
<div key={field.name}>
{field.type !== "hidden" && (
<label htmlFor={field.name} className="block text-sm font-medium text-gray-700 mb-1">
{fieldProps[field.name]?.label ?? field.name}
</label>
)}
<div>
<input
id={field.name}
name={field.name}
type={field.type}
autoComplete={fieldProps[field.name]}
required={field.required}
value={field.value || undefined}
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm"
/>
</div>
</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"
>
Sign up
</button>
</div>
</form>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,28 @@
import { useState } from "react";
import Layout from "../src/components/Layout";
import Table from "../src/components/Table";
const data = [
{ skylink: "PAL0w4SdA5rFCDGEutgpeQ50Om-YkBabtXVOJAkmedslKw", size: "1 KB", date: "today" },
{ skylink: "XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg", size: "102 MB", date: "today" },
{ skylink: "IADUs8d9CQjUO34LmdaaNPK_STuZo24rpKVfYW3wPPM2uQ", size: "141 KB", date: "today" },
{ skylink: "_A2zt5SKoqwnnZU4cBF8uBycSKULXMyeg1c5ZISBr2Q3dA", size: "1 KB", date: "today" },
{ skylink: "AAC0uO43g64ULpyrW0zO3bjEknSFbAhm8c-RFP21EQlmSQ", size: "0 KB", date: "today" },
{ skylink: "CACqf4NlIMlA0CCCieYGjpViPGyfyJ4v1x3bmuCKZX8FKA", size: "1.3 MB", date: "today" },
];
const headers = [
{ key: "skylink", name: "Skylink" },
{ key: "size", name: "Size" },
{ key: "date", name: "Access time" },
];
const actions = [];
export default function Downloads() {
const [page, setPage] = useState(1);
return (
<Layout title="Your downloads">
<Table data={data} headers={headers} actions={actions} page={page} setPage={setPage} />
</Layout>
);
}

View File

@ -0,0 +1,5 @@
import Layout from "../src/components/Layout";
export default function Home() {
return <Layout title="Dashboard"></Layout>;
}

View File

@ -0,0 +1,401 @@
import Layout from "../src/components/Layout";
export default function Payments() {
return (
<Layout title="Payments">
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6">
{/* This example requires Tailwind CSS v2.0+ */}
<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">Free Plan</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">Next invoice</dt>
<dd className="mt-1 text-3xl font-semibold text-gray-900">-</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">Plan usage this month</dt>
<dd className="mt-1 text-3xl font-semibold text-gray-900">24.57%</dd>
</div>
</div>
</dl>
{/* This example requires Tailwind CSS v2.0+ */}
<div className="bg-white">
<div className="max-w-7xl mx-auto py-24 px-4 sm:px-6 lg:px-8">
<div className="sm:flex sm:flex-col sm:align-center">
<h1 className="text-5xl font-extrabold text-gray-900 sm:text-center">Pricing Plans</h1>
<p className="mt-5 text-xl text-gray-500 sm:text-center">
Start building for free, then add a site plan to go live. Account plans unlock additional features.
</p>
</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">Hobby</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">$12</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 Hobby
</a>
</div>
<div className="pt-6 pb-8 px-6">
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What's included</h3>
<ul className="mt-6 space-y-4">
<li className="flex space-x-3">
{/* Heroicon name: 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: 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">Freelancer</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">$24</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 Freelancer
</a>
</div>
<div className="pt-6 pb-8 px-6">
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What's included</h3>
<ul className="mt-6 space-y-4">
<li className="flex space-x-3">
{/* Heroicon name: 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: 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: 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">Startup</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">$32</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 Startup
</a>
</div>
<div className="pt-6 pb-8 px-6">
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What's included</h3>
<ul className="mt-6 space-y-4">
<li className="flex space-x-3">
{/* Heroicon name: 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: 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: 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: 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">Enterprise</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">$48</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 Enterprise
</a>
</div>
<div className="pt-6 pb-8 px-6">
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What's included</h3>
<ul className="mt-6 space-y-4">
<li className="flex space-x-3">
{/* Heroicon name: 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: 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: 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: 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: 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: 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: 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>
</div>
</Layout>
);
}

View File

@ -0,0 +1,28 @@
import { useState } from "react";
import Layout from "../src/components/Layout";
import Table from "../src/components/Table";
const data = [
{ skylink: "PAL0w4SdA5rFCDGEutgpeQ50Om-YkBabtXVOJAkmedslKw", size: "1 KB", date: "today" },
{ skylink: "XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg", size: "102 MB", date: "today" },
{ skylink: "IADUs8d9CQjUO34LmdaaNPK_STuZo24rpKVfYW3wPPM2uQ", size: "141 KB", date: "today" },
{ skylink: "_A2zt5SKoqwnnZU4cBF8uBycSKULXMyeg1c5ZISBr2Q3dA", size: "1 KB", date: "today" },
{ skylink: "AAC0uO43g64ULpyrW0zO3bjEknSFbAhm8c-RFP21EQlmSQ", size: "0 KB", date: "today" },
{ skylink: "CACqf4NlIMlA0CCCieYGjpViPGyfyJ4v1x3bmuCKZX8FKA", size: "1.3 MB", date: "today" },
];
const headers = [
{ key: "skylink", name: "Skylink" },
{ key: "size", name: "Size" },
{ key: "date", name: "Uploaded on" },
];
const actions = [{ key: "delete", name: "Delete", action: () => undefined }];
export default function Uploads() {
const [page, setPage] = useState(1);
return (
<Layout title="Your uploads">
<Table data={data} headers={headers} actions={actions} page={page} setPage={setPage} />
</Layout>
);
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,225 @@
import Link from "next/link";
import Head from "next/head";
import { useState } from "react";
export default function Layout({ title, children }) {
const [menuOpen, openMenu] = useState(false);
const [avatarDropdownOpen, openAvatarDropdown] = useState(false);
return (
<div>
<Head>
<title>Skynet - {title}</title>
<link rel="icon" href="/favicon.ico" />
</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">
<div className="flex-shrink-0">
<svg
viewBox="0 0 66 66"
width={33}
height={33}
className="mx-auto h-12 w-auto"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M52 52V33.287C52 22.784 43.599 14.052 33.096 14 22.544 13.948 13.948 22.543 14 33.096 14.052 43.599 22.784 52 33.287 52H52zM33 1c17.673 0 32 14.326 32 32v32H33C15.326 65 1 50.673 1 33 1 15.326 15.326 1 33 1z"
fillRule="nonzero"
stroke="green"
strokeWidth="2"
fill="none"
/>
</svg>
</div>
<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="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="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="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">
Your downloads
</a>
</Link>
</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"
>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
role="menuitem"
>
Settings (coming soon)
</a>
<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"
role="menuitem"
>
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" */}
<a href="#" className="bg-gray-900 text-white block px-3 py-2 rounded-md text-base font-medium">
Dashboard
</a>
<a
href="#"
className="text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"
>
Your uploads (coming soon)
</a>
<a
href="#"
className="text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"
>
Your downloads (coming soon)
</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">
<a
href="#"
className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700"
>
Settings (coming soon)
</a>
<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="#"
className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700"
>
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">
{/* Replace with your content */}
{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>
)}
{/* /End replace */}
</div>
</main>
</div>
);
}

View File

@ -0,0 +1,89 @@
const rowsPerPage = 10;
export default function Table({ data, headers, actions, page, setPage }) {
const lastPage = Math.ceil(data.length / rowsPerPage);
if (page > lastPage) setPage(lastPage);
const dataSlice = data.slice(rowsPerPage * (page - 1), rowsPerPage * (page - 1) + rowsPerPage);
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 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
{name}
</th>
))}
{actions.map(({ key, name }) => (
<th key={key} scope="col" className="relative px-6 py-3">
<span className="sr-only">{name}</span>
</th>
))}
</tr>
</thead>
<tbody>
{dataSlice.map((row, index) => (
<tr className={index % 2 ? "bg-white" : "bg-gray-50"} key={index}>
{headers.map(({ key, name }) => (
<td key={key} className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{row[key] || "-"}
</td>
))}
{actions.map(({ key, name, action }) => (
<td key={key} className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="#" className="text-indigo-600 hover:text-indigo-900" onClick={action}>
{name}
</a>
</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">{rowsPerPage * (page - 1) + 1}</span> to{" "}
<span className="font-medium">
{rowsPerPage * page > data.length ? data.length : rowsPerPage * page}
</span>{" "}
of <span className="font-medium">{data.length}</span> results
</p>
</div>
<div className="flex-1 flex justify-between sm:justify-end">
<a
href="#"
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
onClick={() => (page > 1 ? setPage(page - 1) : undefined)}
>
Previous
</a>
<a
href="#"
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
onClick={() => (page < lastPage ? setPage(page + 1) : undefined)}
>
Next
</a>
</div>
</nav>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,39 @@
export const SECURITY_MODE_STANDALONE = "cookie";
export const SECURITY_MODE_JWT = "jwt";
const baseUrl = process.env.BASE_URL || "/";
let securityMode = SECURITY_MODE_STANDALONE;
let browserUrl = process.env.KRATOS_BROWSER_URL || "https://secure.siasky.xyz/.ory/kratos/public";
let publicUrl = process.env.KRATOS_PUBLIC_URL || "https://secure.siasky.xyz/.ory/kratos/public";
switch ((process.env.SECURITY_MODE || "").toLowerCase()) {
case "jwt":
case "oathkeeper":
securityMode = SECURITY_MODE_JWT;
break;
case "cookie":
case "standalone":
default:
securityMode = SECURITY_MODE_STANDALONE;
}
export default {
kratos: {
browser: browserUrl.replace(/\/+$/, ""),
admin: (process.env.KRATOS_ADMIN_URL || "").replace(/\/+$/, ""),
public: publicUrl.replace(/\/+$/, ""),
},
baseUrl,
jwksUrl: process.env.JWKS_URL || "/",
projectName: process.env.PROJECT_NAME || "SecureApp",
securityMode,
SECURITY_MODE_JWT,
SECURITY_MODE_STANDALONE,
https: {
enabled: process.env.hasOwnProperty("TLS_KEY_PATH") && process.env.hasOwnProperty("TLS_CERT_PATH"),
certificatePath: process.env.TLS_CERT_PATH || "",
keyPath: process.env.TLS_KEY_PATH || "",
},
};

View File

@ -0,0 +1,122 @@
.container {
min-height: 100vh;
padding: 0 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
width: 100%;
height: 100px;
border-top: 1px solid #eaeaea;
display: flex;
justify-content: center;
align-items: center;
}
.footer img {
margin-left: 0.5rem;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New,
monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
margin-top: 3rem;
}
.card {
margin: 1rem;
flex-basis: 45%;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h3 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}

View File

@ -0,0 +1,16 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans,
Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}

View File

@ -0,0 +1,11 @@
module.exports = {
purge: ["./pages/**/*.js", "./components/**/*.js"],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};