feat: add website
This commit is contained in:
parent
54e5e09024
commit
f0acd9ebc1
|
@ -4,8 +4,11 @@ on:
|
||||||
push:
|
push:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '*.md'
|
- '*.md'
|
||||||
|
- website/**
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- website/**
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,12 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- website/**
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- website/**
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
name: Website
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- website/**
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- website/**
|
||||||
|
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Next.js Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2.0.1
|
||||||
|
id: pnpm-install
|
||||||
|
with:
|
||||||
|
version: 7
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
id: pnpm-cache
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||||
|
|
||||||
|
- name: Cache pnpm
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('website/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Cache next.js
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ github.workspace }}/.next/cache
|
||||||
|
key: ${{ runner.os }}-nextjs-${{ hashFiles('website/pnpm-lock.yaml') }}-${{ hashFiles('website/**.[jt]s', 'website/**.[jt]sx') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-nextjs-${{ hashFiles('website/pnpm-lock.yaml') }}-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build page
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Export page
|
||||||
|
run: pnpm run export
|
|
@ -1,21 +1,17 @@
|
||||||
use std::path::PathBuf;
|
use std::fs;
|
||||||
use std::{fs, io};
|
|
||||||
|
|
||||||
use crate::fs::validate_path;
|
use crate::fs::validate_path;
|
||||||
use axum::extract::Query;
|
use axum::extract::Query;
|
||||||
use axum::{extract::rejection::JsonRejection, Extension, Json};
|
use axum::Extension;
|
||||||
use axum_auth::AuthBearer;
|
use axum_auth::AuthBearer;
|
||||||
use byte_unit::Byte;
|
|
||||||
use homedisk_database::Database;
|
use homedisk_database::Database;
|
||||||
use homedisk_types::fs::list::DirInfo;
|
|
||||||
use homedisk_types::fs::upload::Pagination;
|
use homedisk_types::fs::upload::Pagination;
|
||||||
use homedisk_types::{
|
use homedisk_types::{
|
||||||
config::types::Config,
|
config::types::Config,
|
||||||
errors::{FsError, ServerError},
|
errors::ServerError,
|
||||||
fs::list::{FileInfo, Request, Response},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::middleware::{find_user, validate_json, validate_jwt};
|
use crate::middleware::{find_user, validate_jwt};
|
||||||
|
|
||||||
pub async fn handle(
|
pub async fn handle(
|
||||||
Extension(db): Extension<Database>,
|
Extension(db): Extension<Database>,
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = false
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
# 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
|
|
@ -0,0 +1,8 @@
|
||||||
|
import axios from "axios"
|
||||||
|
import config from "../config"
|
||||||
|
|
||||||
|
const instance = axios.create({
|
||||||
|
baseURL: config.apiUrl,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default instance
|
|
@ -0,0 +1,4 @@
|
||||||
|
import list from "./list"
|
||||||
|
import login from "./login"
|
||||||
|
|
||||||
|
export default { list, login }
|
|
@ -0,0 +1,34 @@
|
||||||
|
import axios from './axios'
|
||||||
|
|
||||||
|
export default async function list(path: string, token: string): Promise<any> {
|
||||||
|
const request = axios.post("/fs/list", {
|
||||||
|
path
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = request
|
||||||
|
.then(response => {
|
||||||
|
const { data } = response
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.response?.data?.error_message) {
|
||||||
|
const error = err.response.data.error_message
|
||||||
|
|
||||||
|
if (error.toString() == "[object Object]") {
|
||||||
|
Object.keys(error).forEach(key => {
|
||||||
|
throw new Error(key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import axios from './axios'
|
||||||
|
|
||||||
|
export default async function login(username: string, password: string): Promise<string> {
|
||||||
|
const request = axios.post("/auth/login", {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = request
|
||||||
|
.then(response => {
|
||||||
|
const { data } = response
|
||||||
|
|
||||||
|
return data.LoggedIn.access_token
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.response?.data?.error_message) {
|
||||||
|
const error = err.response.data.error_message
|
||||||
|
|
||||||
|
if (error.toString() == "[object Object]") {
|
||||||
|
Object.keys(error).forEach(key => {
|
||||||
|
throw new Error(key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: ${({ theme }) => theme.colors.background};
|
||||||
|
color: ${({ theme }) => theme.colors.color};
|
||||||
|
`
|
||||||
|
|
||||||
|
export default Container
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { GitHub } from "@mui/icons-material"
|
||||||
|
import { IconButton } from "@mui/material"
|
||||||
|
import styled from "styled-components"
|
||||||
|
import { links } from "../config"
|
||||||
|
|
||||||
|
const StyledFooter = styled.footer`
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
border-top: 1px solid ${({ theme }) => theme.footer.borderTop};
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: ${({ theme }) => theme.colors.background};
|
||||||
|
color: ${({ theme }) => theme.colors.color};
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: cente;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
return (
|
||||||
|
<StyledFooter>
|
||||||
|
<IconButton color="inherit">
|
||||||
|
<a href={links.github} target="_blank" rel="noreferrer" color="inherit">
|
||||||
|
<GitHub />
|
||||||
|
</a>
|
||||||
|
</IconButton>
|
||||||
|
</StyledFooter>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { faM, faMoon, faSun } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { RocketLaunch } from "@mui/icons-material";
|
||||||
|
import { AppBar, IconButton, Link, Stack, Toolbar, Typography } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function Footer({ toggleTheme, theme}: Props) {
|
||||||
|
return (
|
||||||
|
<AppBar
|
||||||
|
position="static"
|
||||||
|
sx={{ marginBottom: "calc(2% + 10px)" }}
|
||||||
|
>
|
||||||
|
<Toolbar>
|
||||||
|
<Link href="/" color="inherit">
|
||||||
|
<IconButton
|
||||||
|
size="large"
|
||||||
|
edge="start"
|
||||||
|
color="inherit"
|
||||||
|
aria-label="logo"
|
||||||
|
>
|
||||||
|
<RocketLaunch />
|
||||||
|
</IconButton>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||||
|
HomeDisk
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
<div onClick={() => toggleTheme()}>
|
||||||
|
<FontAwesomeIcon icon={theme == "light" ? faMoon : faSun} />
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
toggleTheme: () => any,
|
||||||
|
theme: string
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const Main = styled.main`
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default Main
|
|
@ -0,0 +1,8 @@
|
||||||
|
export const links = {
|
||||||
|
github: "https://github.com/HomeDisk/cloud"
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
apiUrl: "/api",
|
||||||
|
links,
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
|
@ -0,0 +1,10 @@
|
||||||
|
module.exports = {
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/api/:slug*',
|
||||||
|
destination: 'http://127.0.0.1:8080/:slug*'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"name": "@homedisk/website",
|
||||||
|
"description": "Fast and lightweight local cloud for your data written in Rust",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"author": "MedzikUser <nivua1fn@duck.com>",
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"export": "next export",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "7.17.10",
|
||||||
|
"@emotion/react": "11.9.0",
|
||||||
|
"@emotion/styled": "11.8.1",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "6.1.1",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "6.1.1",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||||
|
"@mui/icons-material": "5.6.2",
|
||||||
|
"@mui/material": "5.6.4",
|
||||||
|
"@mui/styled-engine-sc": "5.6.1",
|
||||||
|
"axios": "0.27.2",
|
||||||
|
"next": "12.1.6",
|
||||||
|
"react": "18.1.0",
|
||||||
|
"react-cookie": "4.1.1",
|
||||||
|
"react-dom": "18.1.0",
|
||||||
|
"react-is": "18.1.0",
|
||||||
|
"styled-components": "5.3.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "17.0.31",
|
||||||
|
"@types/react": "18.0.9",
|
||||||
|
"@types/styled-components": "5.1.25",
|
||||||
|
"eslint": "8.15.0",
|
||||||
|
"eslint-config-next": "12.1.6",
|
||||||
|
"typescript": "4.6.4"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { Button } from '@mui/material'
|
||||||
|
import Head from 'next/head'
|
||||||
|
import { useCookies } from 'react-cookie'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { links } from '../config'
|
||||||
|
import ThemeInterface from '../types/theme'
|
||||||
|
|
||||||
|
const Title = styled.h1`
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.15;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: ${({ theme }) => theme.pages.index.title.a};
|
||||||
|
text-decoration: none;
|
||||||
|
animation: animate 1.5s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animate {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover,
|
||||||
|
a:focus,
|
||||||
|
a:active {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const StyledButton = styled(Button)`
|
||||||
|
margin-top: 1rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>404 - HomeDisk</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<Title>
|
||||||
|
404 | This page could not be found.
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<StyledButton href="/">
|
||||||
|
Go to Home Page
|
||||||
|
</StyledButton>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
import { Button, ThemeProvider as MuiThemeProvider, createTheme as muiCreateTheme, PaletteMode } from '@mui/material'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useCookies } from 'react-cookie'
|
||||||
|
import { createGlobalStyle, ThemeProvider } from 'styled-components'
|
||||||
|
import Container from '../components/container'
|
||||||
|
import Footer from '../components/footer'
|
||||||
|
import Header from '../components/header'
|
||||||
|
import Main from '../components/main'
|
||||||
|
|
||||||
|
const GlobalStyle = createGlobalStyle`
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const lightTheme = {
|
||||||
|
colors: {
|
||||||
|
background: "#ffffff",
|
||||||
|
color: "#000000",
|
||||||
|
error: "#f85b5b"
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
index: {
|
||||||
|
cards: {
|
||||||
|
signin: "#0a60cf",
|
||||||
|
register: "#a06800",
|
||||||
|
files: "#3d8011",
|
||||||
|
settings: "#75006f"
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
a: "#a109c0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
borderTop: "#eaeaea"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const darkTheme = {
|
||||||
|
colors: {
|
||||||
|
background: "#131212",
|
||||||
|
color: "#ffffff",
|
||||||
|
error: "#f85b5b"
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
index: {
|
||||||
|
cards: {
|
||||||
|
signin: "#0a60cf",
|
||||||
|
register: "#a06800",
|
||||||
|
files: "#54ad19",
|
||||||
|
settings: "#c90dbf"
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
a: "#a109c0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
borderTop: "#161616"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App({ Component, pageProps }) {
|
||||||
|
const [cookies, setCookies] = useCookies(["theme"])
|
||||||
|
|
||||||
|
const [theme, setTheme] = useState(lightTheme)
|
||||||
|
const [themeName, setThemeName]: [PaletteMode, any] = useState("light")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cookies.theme) setCookies("theme", "light")
|
||||||
|
|
||||||
|
if (cookies.theme == "dark"){
|
||||||
|
setTheme(darkTheme)
|
||||||
|
setThemeName("dark")
|
||||||
|
}
|
||||||
|
}, [setCookies, setTheme, setThemeName, cookies])
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
if (cookies.theme == "light") {
|
||||||
|
setTheme(darkTheme)
|
||||||
|
setThemeName("dark")
|
||||||
|
|
||||||
|
setCookies("theme", "dark")
|
||||||
|
}
|
||||||
|
if (cookies.theme == "dark") {
|
||||||
|
setTheme(lightTheme)
|
||||||
|
setThemeName("light")
|
||||||
|
|
||||||
|
setCookies("theme", "light")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const muiThene = muiCreateTheme({
|
||||||
|
palette: {
|
||||||
|
mode: themeName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<GlobalStyle />
|
||||||
|
|
||||||
|
<MuiThemeProvider theme={muiThene}>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<Container>
|
||||||
|
<Header toggleTheme={toggleTheme} theme={themeName} />
|
||||||
|
|
||||||
|
<Main>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</Main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</Container>
|
||||||
|
</ThemeProvider>
|
||||||
|
</MuiThemeProvider>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import Document, { DocumentContext, DocumentInitialProps } from 'next/document'
|
||||||
|
import { ServerStyleSheet } from 'styled-components'
|
||||||
|
|
||||||
|
export default class MyDocument extends Document {
|
||||||
|
static async getInitialProps(
|
||||||
|
ctx: DocumentContext
|
||||||
|
): Promise<DocumentInitialProps> {
|
||||||
|
const sheet = new ServerStyleSheet()
|
||||||
|
const originalRenderPage = ctx.renderPage
|
||||||
|
|
||||||
|
try {
|
||||||
|
ctx.renderPage = () =>
|
||||||
|
originalRenderPage({
|
||||||
|
enhanceApp: (App) => (props) =>
|
||||||
|
sheet.collectStyles(<App {...props} />),
|
||||||
|
})
|
||||||
|
|
||||||
|
const initialProps = await Document.getInitialProps(ctx)
|
||||||
|
return {
|
||||||
|
...initialProps,
|
||||||
|
styles: [
|
||||||
|
<>
|
||||||
|
{initialProps.styles}
|
||||||
|
{sheet.getStyleElement()}
|
||||||
|
</>,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
sheet.seal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,169 @@
|
||||||
|
import Head from 'next/head'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useCookies } from 'react-cookie'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { links } from '../config'
|
||||||
|
import ThemeInterface from '../types/theme'
|
||||||
|
|
||||||
|
const Title = styled.h1`
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.15;
|
||||||
|
font-size: 4rem;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: ${({ theme }) => theme.pages.index.title.a};
|
||||||
|
text-decoration: none;
|
||||||
|
animation: animate 1.5s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animate {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover,
|
||||||
|
a:focus,
|
||||||
|
a:active {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Description = styled.p`
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Grid = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
max-width: 800px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Card = styled.div`
|
||||||
|
margin: 1rem;
|
||||||
|
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;
|
||||||
|
max-width: 300px;
|
||||||
|
|
||||||
|
:hover,
|
||||||
|
:focus,
|
||||||
|
:active {
|
||||||
|
color: #0070f3;
|
||||||
|
border-color: #0070f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const CardSignIn = styled.a`
|
||||||
|
color: ${({ theme }) => theme.pages.index.cards.signin};
|
||||||
|
`
|
||||||
|
|
||||||
|
const CardRegister = styled.a`
|
||||||
|
color: ${({ theme }) => theme.pages.index.cards.register};
|
||||||
|
`
|
||||||
|
|
||||||
|
const CardFiles = styled.a`
|
||||||
|
color: ${({ theme }) => theme.pages.index.cards.files};
|
||||||
|
`
|
||||||
|
|
||||||
|
const CardSettings = styled.a`
|
||||||
|
color: ${({ theme }) => theme.pages.index.cards.settings};
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [cookies] = useCookies(["token"])
|
||||||
|
|
||||||
|
const [cards, setCards] = useState(<CardsNonLogged />)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cookies.token) {
|
||||||
|
setCards(<CardsLogged />)
|
||||||
|
}
|
||||||
|
}, [setCards, cookies])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>HomeDisk</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<Title>
|
||||||
|
Welcome to <a href={links.github} target="_blank" rel="noreferrer">HomeDisk!</a>
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<Description>
|
||||||
|
Fast and lightweight local cloud for your data written in Rust
|
||||||
|
</Description>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
{cards}
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardsNonLogged() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card>
|
||||||
|
<CardSignIn href="/login">
|
||||||
|
<h2>Sign in →</h2>
|
||||||
|
<p>Log in to your account</p>
|
||||||
|
</CardSignIn>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardRegister href="/register">
|
||||||
|
<h2>Register →</h2>
|
||||||
|
<p>Register a new account</p>
|
||||||
|
</CardRegister>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardsLogged() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card>
|
||||||
|
<CardFiles href="/user/files">
|
||||||
|
<h2>Files →</h2>
|
||||||
|
<p>View your files</p>
|
||||||
|
</CardFiles>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardSettings href="/user/settings">
|
||||||
|
<h2>Settings →</h2>
|
||||||
|
<p>Go to user settings</p>
|
||||||
|
</CardSettings>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
import { Button, TextField } from '@mui/material'
|
||||||
|
import Head from 'next/head'
|
||||||
|
import Router from 'next/router'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useCookies } from 'react-cookie'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import api from '../api'
|
||||||
|
|
||||||
|
const Title = styled.h1`
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.15;
|
||||||
|
font-size: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: ${({ theme }) => theme.pages.index.title.a};
|
||||||
|
text-decoration: none;
|
||||||
|
animation: animate 1.5s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animate {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover,
|
||||||
|
a:focus,
|
||||||
|
a:active {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const LoginButton = styled(Button)`
|
||||||
|
margin-top: 1rem;
|
||||||
|
align-content: "center";
|
||||||
|
`
|
||||||
|
|
||||||
|
const ErrorComponent = styled.div`
|
||||||
|
color: ${({ theme }) => theme.colors.error};
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const [cookie, setCookie] = useCookies(["token"])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cookie.token) {
|
||||||
|
Router.push('/user/files')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
const [username, setUsername] = useState("")
|
||||||
|
const [password, setPassword] = useState("")
|
||||||
|
|
||||||
|
const handleUsernameChange: React.ChangeEventHandler<HTMLInputElement> = event => {
|
||||||
|
const value = event.target.value
|
||||||
|
setUsername(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePasswordChange: React.ChangeEventHandler<HTMLInputElement> = event => {
|
||||||
|
const value = event.target.value
|
||||||
|
setPassword(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle click "Enter (Return)"
|
||||||
|
const handleKeyPress = (event: React.KeyboardEvent) => {
|
||||||
|
if (event.keyCode === 13 || event.which === 13 || event.charCode === 13) {
|
||||||
|
handleLogin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = () => {
|
||||||
|
const request = api.login(username, password)
|
||||||
|
|
||||||
|
request
|
||||||
|
.then(token => {
|
||||||
|
setCookie("token", token)
|
||||||
|
setError("")
|
||||||
|
})
|
||||||
|
.catch(err => setError(err.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Login - HomeDisk</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<Title>
|
||||||
|
Sign in
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
{error != "" && (
|
||||||
|
<ErrorComponent>{error}</ErrorComponent>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Username"
|
||||||
|
placeholder="Username"
|
||||||
|
margin="normal"
|
||||||
|
value={username}
|
||||||
|
onChange={handleUsernameChange}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Password"
|
||||||
|
placeholder="Password"
|
||||||
|
type="password"
|
||||||
|
margin="normal"
|
||||||
|
value={password}
|
||||||
|
onChange={handlePasswordChange}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LoginButton
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
color="secondary"
|
||||||
|
onClick={handleLogin}
|
||||||
|
disabled={username == "" || password == ""}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</LoginButton>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
import { faFile, faFolder } from "@fortawesome/free-solid-svg-icons"
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
|
import { Link as MuiLink } from "@mui/material"
|
||||||
|
import Head from 'next/head'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useCookies } from 'react-cookie'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import api from '../../api'
|
||||||
|
|
||||||
|
const Table = styled.table`
|
||||||
|
border: 1px solid;
|
||||||
|
width: 80vw;
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
thead {
|
||||||
|
background-color: #01754b;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr,
|
||||||
|
td {
|
||||||
|
border: 1px solid #000;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Icon = styled(FontAwesomeIcon)`
|
||||||
|
padding-right: 5px;
|
||||||
|
padding-left: 5px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function Files() {
|
||||||
|
const [cookies] = useCookies(["token"])
|
||||||
|
|
||||||
|
const [path, setPath] = useState("")
|
||||||
|
const [files, setFiles] = useState([{ name: "", size: "", modified: "" }])
|
||||||
|
const [folders, setFolders] = useState([{ name: "", size: "", modified: "" }])
|
||||||
|
|
||||||
|
const refresh = (path: string) => {
|
||||||
|
api.list(path, cookies.token)
|
||||||
|
.then(data => {
|
||||||
|
setPath(path)
|
||||||
|
setFolders(data.dirs)
|
||||||
|
setFiles(data.files)
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
const path = params.get("dir") || ""
|
||||||
|
|
||||||
|
api.list(path, cookies.token)
|
||||||
|
.then(data => {
|
||||||
|
setPath(path)
|
||||||
|
setFolders(data.dirs)
|
||||||
|
setFiles(data.files)
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err))
|
||||||
|
}, [cookies])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Files - HomeDisk</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<Table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Name</td>
|
||||||
|
<td>Size</td>
|
||||||
|
<td>Modified</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{folders.map((f, index) => <FolderComponent key={index} name={f.name} path={`${path}/${f.name}`} size={f.size} modified={f.modified} refresh={refresh} />)}
|
||||||
|
{files.map((f, index) => <FileComponent key={index} name={f.name} path={`${path}/${f.name}`} size={f.size} modified={f.modified} refresh={refresh} />)}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FolderComponent({ name, path, size, modified, refresh }: Props) {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Link href={`?dir=${path}`}>
|
||||||
|
<MuiLink onClick={() => refresh(path)}>
|
||||||
|
<Icon icon={faFolder} />
|
||||||
|
{name}
|
||||||
|
</MuiLink>
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
<td>{size}</td>
|
||||||
|
<td>{modified}</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FileComponent({ name, path, size, modified, refresh }: Props) {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Link href={`?dir=${path}`}>
|
||||||
|
<MuiLink onClick={() => refresh(path)}>
|
||||||
|
<Icon icon={faFile} />
|
||||||
|
{name}
|
||||||
|
</MuiLink>
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
<td>{size}</td>
|
||||||
|
<td>{modified}</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: string,
|
||||||
|
path: string,
|
||||||
|
size: string,
|
||||||
|
modified: string,
|
||||||
|
refresh: (path: string) => any
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"incremental": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve"
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "pages/404.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
interface ThemeInterface {
|
||||||
|
colors: {
|
||||||
|
background: string;
|
||||||
|
color: string;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
pages: {
|
||||||
|
index: {
|
||||||
|
cards: {
|
||||||
|
signin: string;
|
||||||
|
register: string;
|
||||||
|
};
|
||||||
|
title: {
|
||||||
|
a: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
footer: {
|
||||||
|
borderTop: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThemeInterface
|
Loading…
Reference in New Issue