INIT: Finally adding to git. This is turning out pretty sick!
This commit is contained in:
commit
a60396545d
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
8
.vite/deps/_metadata.json
Normal file
8
.vite/deps/_metadata.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"hash": "7233c84e",
|
||||
"configHash": "bde3dc57",
|
||||
"lockfileHash": "e3b0c442",
|
||||
"browserHash": "124b5502",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
}
|
||||
3
.vite/deps/package.json
Normal file
3
.vite/deps/package.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
1730
backend/package-lock.json
generated
Normal file
1730
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
backend/package.json
Normal file
25
backend/package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "npx tsc",
|
||||
"start": "node dist/server.js",
|
||||
"dev": "ts-node-dev src/server.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.13.9",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
11
backend/src/entry.ts
Normal file
11
backend/src/entry.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @desc An interface that represents an entry in the file system.
|
||||
* @prop path {string} Path to the entry, name is included.
|
||||
* @prop name {string} Name of the entry.
|
||||
* @prop directory {boolean} Entry is a directory.
|
||||
*/
|
||||
export interface entry {
|
||||
path: string;
|
||||
name: string;
|
||||
directory: boolean;
|
||||
}
|
||||
11
backend/src/healthcheck.ts
Normal file
11
backend/src/healthcheck.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @desc Interface for creating and returning a healthcheck
|
||||
* @prop health {string} The status of the server.
|
||||
* @prop errors {string[]} If there are any errors, they're here.
|
||||
* @prop directory_found {boolean} Can the server find the root.
|
||||
*/
|
||||
export interface Healthcheck {
|
||||
health: string;
|
||||
errors: string[];
|
||||
directory_found: boolean;
|
||||
}
|
||||
12
backend/src/log.ts
Normal file
12
backend/src/log.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import {NextFunction, Request, Response} from "express";
|
||||
|
||||
/**
|
||||
* Log a request in a common format. This is designed to be used as middleware.
|
||||
* @param req {Request} The request object.
|
||||
* @param res {Response} The response object.
|
||||
* @param next {NextFunction} Next function
|
||||
*/
|
||||
export function LogRequestMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
console.log(`[${req.method.toUpperCase()}] ${req.url} ${req.ip} `);
|
||||
next();
|
||||
}
|
||||
96
backend/src/server.ts
Normal file
96
backend/src/server.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import express, {Express, Request, Response, Router} from "express";
|
||||
import {Healthcheck} from "./healthcheck";
|
||||
import {printEndpoints} from "./utils";
|
||||
import {LogRequestMiddleware} from "./log";
|
||||
import * as fs from "node:fs";
|
||||
import {entry} from "./entry";
|
||||
import cors from "cors";
|
||||
|
||||
/**
|
||||
* App details
|
||||
*/
|
||||
const PORT = 5000;
|
||||
const APP: Express = express();
|
||||
const ROOT: string = "/home/azpect";
|
||||
|
||||
/**
|
||||
* Configure cors
|
||||
* TODO: Update hosts for production
|
||||
*/
|
||||
const corsOptions: cors.CorsOptions = {
|
||||
origin: ["http://localhost:5173"],
|
||||
methods: ["GET"]
|
||||
};
|
||||
APP.use(cors(corsOptions));
|
||||
|
||||
/**
|
||||
* Apply middleware, this must be done before the routes are created.
|
||||
*/
|
||||
APP.use(LogRequestMiddleware);
|
||||
|
||||
/**
|
||||
* Create routes for modular routing
|
||||
*/
|
||||
const v1: Router = express.Router();
|
||||
|
||||
/**
|
||||
* Return a healthcheck interface loaded with the server's health
|
||||
*/
|
||||
v1.get("/healthcheck", (req: Request, res: Response): void => {
|
||||
let hc: Healthcheck = {
|
||||
health: "Server is in bad health",
|
||||
errors: ["The root directory could not be found."],
|
||||
directory_found: false,
|
||||
};
|
||||
|
||||
res.status(200).json(hc);
|
||||
});
|
||||
|
||||
/**
|
||||
* Index route
|
||||
*/
|
||||
v1.get("/", (req: Request, res: Response): void => {
|
||||
res.send("Hello world!");
|
||||
});
|
||||
|
||||
v1.get("/children", (req: Request, res: Response): void => {
|
||||
// Get the path, if it was not provided, use the root
|
||||
const path: string = (req.query.path || ROOT) as string;
|
||||
// if (!path) {
|
||||
// res.status(400).json({error: "Please provide a path. E.g. /v1/children?path=/path/to/target", code: 400});
|
||||
// return;
|
||||
// }
|
||||
|
||||
// An array of names which are the children
|
||||
const children_paths: string[] = fs.readdirSync(path);
|
||||
|
||||
// Store a list of the children as entries
|
||||
const children: entry[] = [];
|
||||
|
||||
for (const child of children_paths) {
|
||||
try {
|
||||
const isDir: boolean = fs.statSync(path.concat("/", child)).isDirectory();
|
||||
children.push({
|
||||
name: child,
|
||||
path: path.concat("/", child),
|
||||
directory: isDir
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
res.status(200).json(children);
|
||||
});
|
||||
|
||||
/**
|
||||
* Apply the routes to the server
|
||||
*/
|
||||
APP.use("/v1", v1);
|
||||
|
||||
/**
|
||||
* Start the server
|
||||
*/
|
||||
APP.listen(PORT, (): void => {
|
||||
printEndpoints(APP);
|
||||
console.log(`Server listening on :${PORT}`);
|
||||
});
|
||||
27
backend/src/utils.ts
Normal file
27
backend/src/utils.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import {Express} from "express";
|
||||
|
||||
/**
|
||||
* @desc Print the endpoints loaded into the express application.
|
||||
* @param app {Express} Express application
|
||||
*/
|
||||
export function printEndpoints(app: Express): void {
|
||||
function traverse(stack: any[], basePath = ''): void {
|
||||
stack.forEach((layer: any) => {
|
||||
if (layer.route) {
|
||||
const methods = layer.route.methods;
|
||||
if (methods) {
|
||||
Object.keys(methods).forEach((method) => {
|
||||
console.log(`${method.toUpperCase()} ${basePath}${layer.route.path}`);
|
||||
});
|
||||
}
|
||||
} else if (layer.name === 'router' && layer.handle.stack) {
|
||||
const routerBasePath = layer.regexp
|
||||
? basePath + layer.regexp.source.replace('\\/?(?=\\/|$)', '').replace('\\/', '/')
|
||||
: basePath;
|
||||
traverse(layer.handle.stack, routerBasePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
traverse(app._router.stack);
|
||||
}
|
||||
121
backend/tsconfig.json
Normal file
121
backend/tsconfig.json
Normal file
@ -0,0 +1,121 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016",
|
||||
/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "libReplacement": true, /* Enable lib replacement. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs",
|
||||
/* Specify what module code is generated. */
|
||||
"rootDir": "./src",
|
||||
/* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist",
|
||||
/* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true,
|
||||
/* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
/* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true,
|
||||
/* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true
|
||||
/* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
12
frontend/README.md
Normal file
12
frontend/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript and enable type-aware lint rules. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
33
frontend/eslint.config.js
Normal file
33
frontend/eslint.config.js
Normal file
@ -0,0 +1,33 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
14
frontend/index.html
Normal file
14
frontend/index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React</title>
|
||||
<link rel="stylesheet" href="/src/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
3544
frontend/package-lock.json
generated
Normal file
3544
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
frontend/package.json
Normal file
33
frontend/package.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "serverfilemanager",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.0.9",
|
||||
"@tanstack/react-query": "^5.67.1",
|
||||
"axios": "^1.8.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwindcss": "^4.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^15.15.0",
|
||||
"postcss": "^8.5.3",
|
||||
"react-router-dom": "^7.2.0",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
1
frontend/public/vite.svg
Normal file
1
frontend/public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
26
frontend/src/App.jsx
Normal file
26
frontend/src/App.jsx
Normal file
@ -0,0 +1,26 @@
|
||||
import './index.css'
|
||||
import React from "react";
|
||||
import {BrowserRouter, Route, Routes} from "react-router-dom";
|
||||
import Login from "./pages/Login.jsx";
|
||||
import NotFound from "./pages/NotFound.jsx";
|
||||
import Dashboard from "./pages/Dashboard.jsx";
|
||||
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
{/*<Route path="/" element={<> /!* Layout, if I create one *!/ </>}/>*/}
|
||||
<Route path="/" element={<Login/>}/>
|
||||
<Route path="login" element={<Login/>}/>
|
||||
<Route path="dashboard" element={<Dashboard/>}/>
|
||||
<Route path="*" element={<NotFound/>}/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
1
frontend/src/assets/react.svg
Normal file
1
frontend/src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
68
frontend/src/components/ PathDisplay.jsx
Normal file
68
frontend/src/components/ PathDisplay.jsx
Normal file
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Takes the user back to the home directory. The onClick prop
|
||||
* is called when the button is clicked.
|
||||
* @param onClick {function}
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function HomeButton({onClick}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="hover:bg-gray-200 p-1.5 rounded-full transition-colors duration-150">
|
||||
<svg className="text-black h-4"
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor">
|
||||
<path d="M1 6V15H6V11C6 9.89543 6.89543 9 8 9C9.10457 9 10 9.89543 10 11V15H15V6L8 0L1 6Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function BackButton({onClick}) {
|
||||
return (
|
||||
<button onClick={onClick}
|
||||
className="hover:bg-gray-200 p-1.5 mr-1 rounded-full transition-colors duration-150">
|
||||
<svg className="h-5 text-black" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4 10L3.29289 10.7071L2.58579 10L3.29289 9.29289L4 10ZM21 18C21 18.5523 20.5523 19 20 19C19.4477 19 19 18.5523 19 18L21 18ZM8.29289 15.7071L3.29289 10.7071L4.70711 9.29289L9.70711 14.2929L8.29289 15.7071ZM3.29289 9.29289L8.29289 4.29289L9.70711 5.70711L4.70711 10.7071L3.29289 9.29289ZM4 9L14 9L14 11L4 11L4 9ZM21 16L21 18L19 18L19 16L21 16ZM14 9C17.866 9 21 12.134 21 16L19 16C19 13.2386 16.7614 11 14 11L14 9Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param name {string}
|
||||
* @param index {number}
|
||||
* @param onClick {function}
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function PathElement({name, index, onClick}) {
|
||||
const handleClick = () => {
|
||||
onClick(index);
|
||||
};
|
||||
return <button onClick={handleClick}>/<span className="hover:underline cursor-pointer">{name}</span></button>
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the path provided in the props.
|
||||
* @param path {string[]}
|
||||
* @param updatePath {function(number)}
|
||||
* @param backHome {function}
|
||||
* @param backArrow {function}
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export default function PathDisplay({path, updatePath, backHome, backArrow}) {
|
||||
return (
|
||||
<div
|
||||
className="w-2/3 mt-8 border-b-1 border-gray-400 bg-white flex items-center truncate">
|
||||
<HomeButton onClick={backHome}/>
|
||||
<BackButton onClick={backArrow}/>
|
||||
{path.map((seg, idx) => <PathElement name={seg} index={idx} onClick={updatePath}/>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
68
frontend/src/components/Directory.jsx
Normal file
68
frontend/src/components/Directory.jsx
Normal file
@ -0,0 +1,68 @@
|
||||
import "../index.css"
|
||||
import {useState} from "react";
|
||||
|
||||
function FileIcon() {
|
||||
return <svg className="text-black h-5 mx-2" stroke="currentColor" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M19 9V17.8C19 18.9201 19 19.4802 18.782 19.908C18.5903 20.2843 18.2843 20.5903 17.908 20.782C17.4802 21 16.9201 21 15.8 21H8.2C7.07989 21 6.51984 21 6.09202 20.782C5.71569 20.5903 5.40973 20.2843 5.21799 19.908C5 19.4802 5 18.9201 5 17.8V6.2C5 5.07989 5 4.51984 5.21799 4.09202C5.40973 3.71569 5.71569 3.40973 6.09202 3.21799C6.51984 3 7.0799 3 8.2 3H13M19 9L13 3M19 9H14C13.4477 9 13 8.55228 13 8V3"
|
||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
}
|
||||
|
||||
function DirectoryIcon() {
|
||||
return <svg className="text-black h-5 mx-2" stroke="currentColor" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3 8.2C3 7.07989 3 6.51984 3.21799 6.09202C3.40973 5.71569 3.71569 5.40973 4.09202 5.21799C4.51984 5 5.0799 5 6.2 5H9.67452C10.1637 5 10.4083 5 10.6385 5.05526C10.8425 5.10425 11.0376 5.18506 11.2166 5.29472C11.4184 5.4184 11.5914 5.59135 11.9373 5.93726L12.0627 6.06274C12.4086 6.40865 12.5816 6.5816 12.7834 6.70528C12.9624 6.81494 13.1575 6.89575 13.3615 6.94474C13.5917 7 13.8363 7 14.3255 7H17.8C18.9201 7 19.4802 7 19.908 7.21799C20.2843 7.40973 20.5903 7.71569 20.782 8.09202C21 8.51984 21 9.0799 21 10.2V15.8C21 16.9201 21 17.4802 20.782 17.908C20.5903 18.2843 20.2843 18.5903 19.908 18.782C19.4802 19 18.9201 19 17.8 19H6.2C5.07989 19 4.51984 19 4.09202 18.782C3.71569 18.5903 3.40973 18.2843 3.21799 17.908C3 17.4802 3 16.9201 3 15.8V8.2Z"
|
||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param name {{name: string, path: string, directory: boolean}}
|
||||
* @param showHidden {boolean}
|
||||
* @param key {number}
|
||||
* @param appendPath {function(string)}
|
||||
* @param toggleSelected {function(string)}
|
||||
* @returns {{}}
|
||||
* @constructor
|
||||
*/
|
||||
export default function Directory({entry, showHidden, key, appendPath, toggleSelected}) {
|
||||
const [selected, setSelected] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
if (entry.directory) {
|
||||
appendPath(entry.name);
|
||||
} else {
|
||||
console.log(`OPENING FILE: ${entry.path}`)
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheck = () => {
|
||||
toggleSelected(entry.name);
|
||||
setSelected(!selected);
|
||||
}
|
||||
|
||||
// This is temporary, eventually I will have a real data model that stores
|
||||
// directory vs file status.
|
||||
// const isDirectory = !name.endsWith(".html");
|
||||
if (entry.name.startsWith(".") && !showHidden) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<label className={`w-full hover:bg-gray-300 flex ${selected ? "bg-blue-300" : "bg-gray-100"}`}>
|
||||
<input className="mx-2 peer checked:bg-blue-300" type="checkbox" checked={selected}
|
||||
onChange={handleCheck}/>
|
||||
<div className="w-full flex bg-gray-100 peer-checked:bg-blue-300 hover:bg-gray-300 peer-hover:bg-gray-300">
|
||||
<button className="flex items-center" onClick={handleClick}>
|
||||
{entry.directory ? <DirectoryIcon/> : <FileIcon/>}
|
||||
<p className="p-2 hover:underline hover:text-blue-400" key={key}>{entry.name}</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</label>
|
||||
);
|
||||
}
|
||||
20
frontend/src/components/DirectoryList.jsx
Normal file
20
frontend/src/components/DirectoryList.jsx
Normal file
@ -0,0 +1,20 @@
|
||||
import Directory from "./Directory.jsx";
|
||||
|
||||
/**
|
||||
* Display the directories in the current path.
|
||||
* @param diretories {string[]} Children of the path.
|
||||
* @param showHidden {boolean} Display hidden items.
|
||||
* @param appendPath {function(string)} Function to add a child to the path.
|
||||
* @param toggleSelected {function(string)} Function to toggle selection status.
|
||||
* @constructor
|
||||
*/
|
||||
export default function DirectoryList({dirs, showHidden, appendPath, toggleSelected}) {
|
||||
return (
|
||||
<>
|
||||
{dirs.map((dir, idx) => <Directory entry={dir} showHidden={showHidden} key={idx} appendPath={appendPath}
|
||||
toggleSelected={toggleSelected}/>)}
|
||||
</>
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
99
frontend/src/components/LoginForm.jsx
Normal file
99
frontend/src/components/LoginForm.jsx
Normal file
@ -0,0 +1,99 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import UserInput from "./UserInput.jsx";
|
||||
import PasswordInput from "./PasswordInput.jsx";
|
||||
import RememberMe from "./RememberMe.jsx";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
export default function LoginForm() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [remember, setRemember] = useState(false);
|
||||
const [password, setPassword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
/**
|
||||
* The name of the value stored in local storage.
|
||||
* @type {string}
|
||||
*/
|
||||
const storage_id = "gophernest_credentials";
|
||||
|
||||
/**
|
||||
* Set the email in the form state.
|
||||
* @param newUsername
|
||||
*/
|
||||
const updateUsername = (newUsername) => {
|
||||
setUsername(newUsername);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the 'remember me' in the form start.
|
||||
* @param newRemember
|
||||
*/
|
||||
const updateRemember = (newRemember) => {
|
||||
setRemember(newRemember);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the password in the form state.
|
||||
* @param newPassword
|
||||
*/
|
||||
const updatePassword = (newPassword) => {
|
||||
setPassword(newPassword);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handle the login submission, data is stored in the local storage.
|
||||
* @param event {SubmitEvent}
|
||||
*/
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
const data = {username, password};
|
||||
|
||||
// TODO: There is no validation yet, need to implement that
|
||||
|
||||
// Store data in session if the user does not want to be remembered
|
||||
if (remember) {
|
||||
const expires = new Date();
|
||||
expires.setMonth(expires.getMonth() + 1);
|
||||
localStorage.setItem(storage_id, JSON.stringify({...data, expires}));
|
||||
} else {
|
||||
sessionStorage.setItem(storage_id, JSON.stringify(data));
|
||||
}
|
||||
|
||||
navigate("/login");
|
||||
|
||||
// Disable loading now, this might take time but right now its instant
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// Redirect if the user is logged in
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem(storage_id) != null || sessionStorage.getItem(storage_id) != null) {
|
||||
navigate("/dashboard");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return <form onSubmit={handleSubmit} className="w-full flex flex-col items-center justify-center">
|
||||
<UserInput onChange={updateUsername}/>
|
||||
<PasswordInput onChange={updatePassword}/>
|
||||
<RememberMe onChange={updateRemember}/>
|
||||
|
||||
{error && <p className="w-full text-red-500 text-sm my-2">{error}</p>}
|
||||
|
||||
<button type="submit"
|
||||
disabled={loading}
|
||||
className="mt-8 bg-blue-400 py-2 text-white w-full rounded-sm disabled:bg-gray-400 disabled:cursor-not-allowed">
|
||||
{loading ? "Loading..." : "Login"}
|
||||
</button>
|
||||
|
||||
<p className="text-gray-400 text-xs text-center mt-6">
|
||||
If you do not have an account, you're in the wrong place!
|
||||
</p>
|
||||
</form>
|
||||
};
|
||||
156
frontend/src/components/Navbar.jsx
Normal file
156
frontend/src/components/Navbar.jsx
Normal file
@ -0,0 +1,156 @@
|
||||
import "../index.css"
|
||||
import {useState} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
/**
|
||||
* Main navbar icon
|
||||
* @param height The height of the icon (h-#)
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function MainIcon() {
|
||||
return <svg className="h-10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 9.5V15.5M12 9.5L10 11.5M12 9.5L14 11.5M8.4 19C5.41766 19 3 16.6044 3 13.6493C3 11.2001 4.8 8.9375 7.5 8.5C8.34694 6.48637 10.3514 5 12.6893 5C15.684 5 18.1317 7.32251 18.3 10.25C19.8893 10.9449 21 12.6503 21 14.4969C21 16.9839 18.9853 19 16.5 19L8.4 19Z"
|
||||
stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
}
|
||||
|
||||
/**
|
||||
* Download button
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function DownloadButton() {
|
||||
return (
|
||||
<button className="text-black" title="Download files">
|
||||
<svg className="hover:bg-gray-300 mx-1 transition-colors duration-200 p-1.5 rounded-full h-8"
|
||||
viewBox="0 0 24 24" fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.5535 16.5061C12.4114 16.6615 12.2106 16.75 12 16.75C11.7894 16.75 11.5886 16.6615 11.4465 16.5061L7.44648 12.1311C7.16698 11.8254 7.18822 11.351 7.49392 11.0715C7.79963 10.792 8.27402 10.8132 8.55352 11.1189L11.25 14.0682V3C11.25 2.58579 11.5858 2.25 12 2.25C12.4142 2.25 12.75 2.58579 12.75 3V14.0682L15.4465 11.1189C15.726 10.8132 16.2004 10.792 16.5061 11.0715C16.8118 11.351 16.833 11.8254 16.5535 12.1311L12.5535 16.5061Z"/>
|
||||
<path
|
||||
d="M3.75 15C3.75 14.5858 3.41422 14.25 3 14.25C2.58579 14.25 2.25 14.5858 2.25 15V15.0549C2.24998 16.4225 2.24996 17.5248 2.36652 18.3918C2.48754 19.2919 2.74643 20.0497 3.34835 20.6516C3.95027 21.2536 4.70814 21.5125 5.60825 21.6335C6.47522 21.75 7.57754 21.75 8.94513 21.75H15.0549C16.4225 21.75 17.5248 21.75 18.3918 21.6335C19.2919 21.5125 20.0497 21.2536 20.6517 20.6516C21.2536 20.0497 21.5125 19.2919 21.6335 18.3918C21.75 17.5248 21.75 16.4225 21.75 15.0549V15C21.75 14.5858 21.4142 14.25 21 14.25C20.5858 14.25 20.25 14.5858 20.25 15C20.25 16.4354 20.2484 17.4365 20.1469 18.1919C20.0482 18.9257 19.8678 19.3142 19.591 19.591C19.3142 19.8678 18.9257 20.0482 18.1919 20.1469C17.4365 20.2484 16.4354 20.25 15 20.25H9C7.56459 20.25 6.56347 20.2484 5.80812 20.1469C5.07435 20.0482 4.68577 19.8678 4.40901 19.591C4.13225 19.3142 3.9518 18.9257 3.85315 18.1919C3.75159 17.4365 3.75 16.4354 3.75 15Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload button
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function UploadButton() {
|
||||
return (
|
||||
<button className="text-black" title="Upload files">
|
||||
<svg className="hover:bg-gray-300 mx-1 transition-colors duration-200 p-1.5 rounded-full font-semibold h-8"
|
||||
viewBox="0 0 24 24" fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.5535 2.49392C12.4114 2.33852 12.2106 2.25 12 2.25C11.7894 2.25 11.5886 2.33852 11.4465 2.49392L7.44648 6.86892C7.16698 7.17462 7.18822 7.64902 7.49392 7.92852C7.79963 8.20802 8.27402 8.18678 8.55352 7.88108L11.25 4.9318V16C11.25 16.4142 11.5858 16.75 12 16.75C12.4142 16.75 12.75 16.4142 12.75 16V4.9318L15.4465 7.88108C15.726 8.18678 16.2004 8.20802 16.5061 7.92852C16.8118 7.64902 16.833 7.17462 16.5535 6.86892L12.5535 2.49392Z"/>
|
||||
<path
|
||||
d="M3.75 15C3.75 14.5858 3.41422 14.25 3 14.25C2.58579 14.25 2.25 14.5858 2.25 15V15.0549C2.24998 16.4225 2.24996 17.5248 2.36652 18.3918C2.48754 19.2919 2.74643 20.0497 3.34835 20.6516C3.95027 21.2536 4.70814 21.5125 5.60825 21.6335C6.47522 21.75 7.57754 21.75 8.94513 21.75H15.0549C16.4225 21.75 17.5248 21.75 18.3918 21.6335C19.2919 21.5125 20.0497 21.2536 20.6517 20.6516C21.2536 20.0497 21.5125 19.2919 21.6335 18.3918C21.75 17.5248 21.75 16.4225 21.75 15.0549V15C21.75 14.5858 21.4142 14.25 21 14.25C20.5858 14.25 20.25 14.5858 20.25 15C20.25 16.4354 20.2484 17.4365 20.1469 18.1919C20.0482 18.9257 19.8678 19.3142 19.591 19.591C19.3142 19.8678 18.9257 20.0482 18.1919 20.1469C17.4365 20.2484 16.4354 20.25 15 20.25H9C7.56459 20.25 6.56347 20.2484 5.80812 20.1469C5.07435 20.0482 4.68577 19.8678 4.40901 19.591C4.13225 19.3142 3.9518 18.9257 3.85315 18.1919C3.75159 17.4365 3.75 16.4354 3.75 15Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Information button. Not sure what this is going to do...
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function InfoButton() {
|
||||
return (
|
||||
<button className="text-black" title="Filesystem information">
|
||||
<svg className="hover:bg-gray-300 mx-1 transition-colors duration-200 p-1.5 rounded-full font-semibold h-8"
|
||||
viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 17.75C12.4142 17.75 12.75 17.4142 12.75 17V11C12.75 10.5858 12.4142 10.25 12 10.25C11.5858 10.25 11.25 10.5858 11.25 11V17C11.25 17.4142 11.5858 17.75 12 17.75Z"/>
|
||||
<path
|
||||
d="M12 7C12.5523 7 13 7.44772 13 8C13 8.55228 12.5523 9 12 9C11.4477 9 11 8.55228 11 8C11 7.44772 11.4477 7 12 7Z"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd"
|
||||
d="M1.25 12C1.25 6.06294 6.06294 1.25 12 1.25C17.9371 1.25 22.75 6.06294 22.75 12C22.75 17.9371 17.9371 22.75 12 22.75C6.06294 22.75 1.25 17.9371 1.25 12ZM12 2.75C6.89137 2.75 2.75 6.89137 2.75 12C2.75 17.1086 6.89137 21.25 12 21.25C17.1086 21.25 21.25 17.1086 21.25 12C21.25 6.89137 17.1086 2.75 12 2.75Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Logout button, which clears the user from the storage's.
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function LogoutButton() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
/**
|
||||
* The name of the value stored in local storage.
|
||||
* @type {string}
|
||||
*/
|
||||
const storage_id = "gophernest_credentials";
|
||||
|
||||
const handleClick = () => {
|
||||
localStorage.removeItem(storage_id);
|
||||
sessionStorage.removeItem(storage_id);
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<button className="text-red-500" title="Logout" onClick={handleClick}>
|
||||
<svg
|
||||
className="text-red-500 hover:bg-red-200 mx-1 transition-colors duration-200 p-1.5 rounded-full font-semibold h-8"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M15 4H18C19.1046 4 20 4.89543 20 6V18C20 19.1046 19.1046 20 18 20H15M8 8L4 12M4 12L8 16M4 12L16 12"
|
||||
strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search bar input, with controlled state.
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function SearchBar() {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
/**
|
||||
* Update the controlled state.
|
||||
* @param event {InputEvent}
|
||||
*/
|
||||
const updateSearch = (event) => {
|
||||
setSearch(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-4 w-1/4">
|
||||
<input
|
||||
className="px-2 py-1 w-full text-sm focus:outline-none focus:shadow-sm shadow-blue-300 transition-shadow duration-200 rounded-sm border-1 border-gray-400"
|
||||
type="search"
|
||||
value={search}
|
||||
onInput={updateSearch}
|
||||
placeholder="Search filesystem"/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Navbar() {
|
||||
return <nav className="absolute w-full p-2 flex items-center border-b-1 border-gray-400 bg-gray-100">
|
||||
<MainIcon/>
|
||||
|
||||
<h3 className="text-xl font-mono px-3">file.gophernest.net</h3>
|
||||
<SearchBar/>
|
||||
|
||||
<div className="min-h-fit ml-auto flex">
|
||||
<DownloadButton/>
|
||||
<UploadButton/>
|
||||
<InfoButton/>
|
||||
<LogoutButton/>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
67
frontend/src/components/PasswordInput.jsx
Normal file
67
frontend/src/components/PasswordInput.jsx
Normal file
@ -0,0 +1,67 @@
|
||||
import {useId, useState} from "react";
|
||||
import "../index.css"
|
||||
|
||||
/**
|
||||
* The email input element. A unique ID is generated. This element will
|
||||
* fill the entire width of its parent. (w-full)
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export default function PasswordInput({onChange}) {
|
||||
// Example of the controlled input state pattern
|
||||
const [password, setPassword] = useState("");
|
||||
const [hidden, setHidden] = useState(true);
|
||||
|
||||
// Generate ID for the input element
|
||||
const id = useId();
|
||||
|
||||
/**
|
||||
* Toggle the hidden state which will determine the type of the input.
|
||||
*/
|
||||
const toggleHidden = () => {
|
||||
setHidden(!hidden);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the password
|
||||
* @param event {InputEvent}
|
||||
*/
|
||||
const updatePassword = (event) => {
|
||||
onChange(password);
|
||||
setPassword(event.target.value);
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className="relative w-full my-2">
|
||||
<input
|
||||
type={hidden ? "password" : "text"}
|
||||
value={password}
|
||||
onChange={updatePassword}
|
||||
name={id}
|
||||
required={true}
|
||||
placeholder="Password"
|
||||
className="border border-gray-300 rounded-sm py-2 px-4 placeholder:italic w-full"/>
|
||||
<button
|
||||
onClick={toggleHidden}
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 px-3 h-full my-auto">
|
||||
{hidden ?
|
||||
<svg className="h-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.68936 6.70456C2.52619 6.32384 2.08528 6.14747 1.70456 6.31064C1.32384 6.47381 1.14747 6.91472 1.31064 7.29544L2.68936 6.70456ZM15.5872 13.3287L15.3125 12.6308L15.5872 13.3287ZM9.04145 13.7377C9.26736 13.3906 9.16904 12.926 8.82185 12.7001C8.47466 12.4742 8.01008 12.5725 7.78417 12.9197L9.04145 13.7377ZM6.37136 15.091C6.14545 15.4381 6.24377 15.9027 6.59096 16.1286C6.93815 16.3545 7.40273 16.2562 7.62864 15.909L6.37136 15.091ZM22.6894 7.29544C22.8525 6.91472 22.6762 6.47381 22.2954 6.31064C21.9147 6.14747 21.4738 6.32384 21.3106 6.70456L22.6894 7.29544ZM19 11.1288L18.4867 10.582V10.582L19 11.1288ZM19.9697 13.1592C20.2626 13.4521 20.7374 13.4521 21.0303 13.1592C21.3232 12.8663 21.3232 12.3914 21.0303 12.0985L19.9697 13.1592ZM11.25 16.5C11.25 16.9142 11.5858 17.25 12 17.25C12.4142 17.25 12.75 16.9142 12.75 16.5H11.25ZM16.3714 15.909C16.5973 16.2562 17.0619 16.3545 17.409 16.1286C17.7562 15.9027 17.8545 15.4381 17.6286 15.091L16.3714 15.909ZM5.53033 11.6592C5.82322 11.3663 5.82322 10.8914 5.53033 10.5985C5.23744 10.3056 4.76256 10.3056 4.46967 10.5985L5.53033 11.6592ZM2.96967 12.0985C2.67678 12.3914 2.67678 12.8663 2.96967 13.1592C3.26256 13.4521 3.73744 13.4521 4.03033 13.1592L2.96967 12.0985ZM12 13.25C8.77611 13.25 6.46133 11.6446 4.9246 9.98966C4.15645 9.16243 3.59325 8.33284 3.22259 7.71014C3.03769 7.3995 2.90187 7.14232 2.8134 6.96537C2.76919 6.87696 2.73689 6.80875 2.71627 6.76411C2.70597 6.7418 2.69859 6.7254 2.69411 6.71533C2.69187 6.7103 2.69036 6.70684 2.68957 6.70503C2.68917 6.70413 2.68896 6.70363 2.68892 6.70355C2.68891 6.70351 2.68893 6.70357 2.68901 6.70374C2.68904 6.70382 2.68913 6.70403 2.68915 6.70407C2.68925 6.7043 2.68936 6.70456 2 7C1.31064 7.29544 1.31077 7.29575 1.31092 7.29609C1.31098 7.29624 1.31114 7.2966 1.31127 7.2969C1.31152 7.29749 1.31183 7.2982 1.31218 7.299C1.31287 7.30062 1.31376 7.30266 1.31483 7.30512C1.31698 7.31003 1.31988 7.31662 1.32353 7.32483C1.33083 7.34125 1.34115 7.36415 1.35453 7.39311C1.38127 7.45102 1.42026 7.5332 1.47176 7.63619C1.57469 7.84206 1.72794 8.13175 1.93366 8.47736C2.34425 9.16716 2.96855 10.0876 3.8254 11.0103C5.53867 12.8554 8.22389 14.75 12 14.75V13.25ZM15.3125 12.6308C14.3421 13.0128 13.2417 13.25 12 13.25V14.75C13.4382 14.75 14.7246 14.4742 15.8619 14.0266L15.3125 12.6308ZM7.78417 12.9197L6.37136 15.091L7.62864 15.909L9.04145 13.7377L7.78417 12.9197ZM22 7C21.3106 6.70456 21.3107 6.70441 21.3108 6.70427C21.3108 6.70423 21.3108 6.7041 21.3109 6.70402C21.3109 6.70388 21.311 6.70376 21.311 6.70368C21.3111 6.70352 21.3111 6.70349 21.3111 6.7036C21.311 6.7038 21.3107 6.70452 21.3101 6.70576C21.309 6.70823 21.307 6.71275 21.3041 6.71924C21.2983 6.73223 21.2889 6.75309 21.2758 6.78125C21.2495 6.83757 21.2086 6.92295 21.1526 7.03267C21.0406 7.25227 20.869 7.56831 20.6354 7.9432C20.1669 8.69516 19.4563 9.67197 18.4867 10.582L19.5133 11.6757C20.6023 10.6535 21.3917 9.56587 21.9085 8.73646C22.1676 8.32068 22.36 7.9668 22.4889 7.71415C22.5533 7.58775 22.602 7.48643 22.6353 7.41507C22.6519 7.37939 22.6647 7.35118 22.6737 7.33104C22.6782 7.32097 22.6818 7.31292 22.6844 7.30696C22.6857 7.30398 22.6867 7.30153 22.6876 7.2996C22.688 7.29864 22.6883 7.29781 22.6886 7.29712C22.6888 7.29677 22.6889 7.29646 22.689 7.29618C22.6891 7.29604 22.6892 7.29585 22.6892 7.29578C22.6893 7.29561 22.6894 7.29544 22 7ZM18.4867 10.582C17.6277 11.3882 16.5739 12.1343 15.3125 12.6308L15.8619 14.0266C17.3355 13.4466 18.5466 12.583 19.5133 11.6757L18.4867 10.582ZM18.4697 11.6592L19.9697 13.1592L21.0303 12.0985L19.5303 10.5985L18.4697 11.6592ZM11.25 14V16.5H12.75V14H11.25ZM14.9586 13.7377L16.3714 15.909L17.6286 15.091L16.2158 12.9197L14.9586 13.7377ZM4.46967 10.5985L2.96967 12.0985L4.03033 13.1592L5.53033 11.6592L4.46967 10.5985Z"
|
||||
fill="#1C274C"/>
|
||||
</svg>
|
||||
:
|
||||
<svg className="h-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M9 4.45962C9.91153 4.16968 10.9104 4 12 4C16.1819 4 19.028 6.49956 20.7251 8.70433C21.575 9.80853 22 10.3606 22 12C22 13.6394 21.575 14.1915 20.7251 15.2957C19.028 17.5004 16.1819 20 12 20C7.81811 20 4.97196 17.5004 3.27489 15.2957C2.42496 14.1915 2 13.6394 2 12C2 10.3606 2.42496 9.80853 3.27489 8.70433C3.75612 8.07914 4.32973 7.43025 5 6.82137"
|
||||
stroke="#1C274C" strokeWidth="1.5" strokeLinecap="round"/>
|
||||
<path
|
||||
d="M15 12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12Z"
|
||||
stroke="#1C274C" strokeWidth="1.5"/>
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
27
frontend/src/components/RememberMe.jsx
Normal file
27
frontend/src/components/RememberMe.jsx
Normal file
@ -0,0 +1,27 @@
|
||||
import {useId, useState} from "react";
|
||||
|
||||
/**
|
||||
* Remember me button toggle button.
|
||||
* @param onChange Function which is used by the parent component.
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export default function RememberMe({onChange}) {
|
||||
const [remember, setRemember] = useState(false);
|
||||
const rememberMeId = useId();
|
||||
|
||||
/**
|
||||
* Toggle the value of the 'remember me' toggle.
|
||||
* It also calls the 'onChange' function which should
|
||||
* be used to store the state in the parent component.
|
||||
*/
|
||||
const toggleRemember = () => {
|
||||
onChange(!remember);
|
||||
setRemember(!remember);
|
||||
};
|
||||
|
||||
return <div className="w-full flex items-center my-2" onClick={toggleRemember}>
|
||||
<input name={rememberMeId} checked={remember} type="checkbox" className="mx-1"/>
|
||||
<label htmlFor={rememberMeId} className="text-sm select-none cursor-pointer">Remember me for 30 days</label>
|
||||
</div>
|
||||
}
|
||||
37
frontend/src/components/UserInput.jsx
Normal file
37
frontend/src/components/UserInput.jsx
Normal file
@ -0,0 +1,37 @@
|
||||
import {useId, useState} from "react";
|
||||
import "../index.css"
|
||||
|
||||
/**
|
||||
* The email input element. A unique ID is generated. This element will
|
||||
* fill the entire width of its parent. (w-full)
|
||||
* @param {onChange}
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export default function UserInput({onChange}) {
|
||||
// Example of the controlled input state pattern
|
||||
const [email, setEmail] = useState("");
|
||||
|
||||
// Generate ID for the input element
|
||||
const id = useId();
|
||||
|
||||
/**
|
||||
* Update the password controlled state
|
||||
* @param event {InputEvent}
|
||||
*/
|
||||
const updateEmail = (event) => {
|
||||
const email = event.target.value;
|
||||
setEmail(email);
|
||||
onChange(email);
|
||||
}
|
||||
|
||||
return <input
|
||||
type="text"
|
||||
name={id}
|
||||
value={email}
|
||||
onChange={updateEmail}
|
||||
placeholder="Username"
|
||||
required={true}
|
||||
className="border border-gray-300 rounded-sm py-2 px-4 placeholder:italic w-full my-2"
|
||||
/>
|
||||
}
|
||||
1
frontend/src/index.css
Normal file
1
frontend/src/index.css
Normal file
@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
10
frontend/src/main.jsx
Normal file
10
frontend/src/main.jsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
114
frontend/src/pages/Dashboard.jsx
Normal file
114
frontend/src/pages/Dashboard.jsx
Normal file
@ -0,0 +1,114 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import DirectoryList from "../components/DirectoryList.jsx";
|
||||
import PathDisplay from "../components/ PathDisplay.jsx";
|
||||
import Navbar from "../components/Navbar.jsx";
|
||||
|
||||
|
||||
export default function Dashboard() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [path, setPath] = useState(["home", "azpect"]);
|
||||
const [showHidden, setShowHidden] = useState(false);
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [files, setFiles] = useState([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const getData = async () => {
|
||||
console.log(`http://localhost:5000/v1/children?path=/${path.join("/")}`);
|
||||
const response = await fetch(`http://localhost:5000/v1/children?path=/${path.join("/")}`)
|
||||
if (!response.ok) {
|
||||
console.error("Something went wrong");
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
getData().then((data) => {
|
||||
setFiles(data);
|
||||
});
|
||||
|
||||
}, [path]);
|
||||
|
||||
/**
|
||||
* The name of the value stored in local storage.
|
||||
* @type {string}
|
||||
*/
|
||||
const storage_id = "gophernest_credentials";
|
||||
|
||||
// Redirect if the user isn't logged in, otherwise update the state.
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem(storage_id) == null && sessionStorage.getItem(storage_id) == null) {
|
||||
navigate("/login");
|
||||
} else {
|
||||
if (localStorage.getItem(storage_id)) {
|
||||
setUsername(JSON.parse(localStorage.getItem(storage_id))["username"]);
|
||||
} else if (sessionStorage.getItem(storage_id)) {
|
||||
setUsername(JSON.parse(sessionStorage.getItem(storage_id))["username"]);
|
||||
}
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
/**
|
||||
* Updates the path by slicing [0:index]
|
||||
* @param index {number} Index to slice to.
|
||||
*/
|
||||
const updatePath = (index) => {
|
||||
setPath(path.slice(0, index + 1));
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the path back to the default, home directory.
|
||||
*/
|
||||
const backHome = () => {
|
||||
setPath(["home", "azpect"]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add name to the path.
|
||||
* @param name Targe child
|
||||
*/
|
||||
const appendPath = (name) => {
|
||||
setPath([...path, name])
|
||||
};
|
||||
|
||||
/**
|
||||
* Back arrow, goes back one directory (cd ..)
|
||||
*/
|
||||
const backArrow = () => {
|
||||
setPath(path.slice(0, path.length - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* This isn't fast, but hopefully the use case will be small batches.
|
||||
* @param file {string} The file to toggle
|
||||
*/
|
||||
const toggleSelected = (file) => {
|
||||
if (!selected.includes(file)) {
|
||||
setSelected([...selected, file]);
|
||||
} else {
|
||||
const idx = selected.indexOf(file);
|
||||
setSelected([...selected.slice(0, idx), ...selected.slice(idx + 1, selected.length)])
|
||||
}
|
||||
};
|
||||
|
||||
// Example of getting absolute paths, for download
|
||||
useEffect(() => {
|
||||
console.log(selected);
|
||||
selected.forEach((file) => {
|
||||
console.log(`/${path.join("/")}/${file}`);
|
||||
});
|
||||
}, [selected]);
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen h-screen pb-8">
|
||||
<Navbar/>
|
||||
<div className="h-full w-full flex flex-col items-center justify-center pb-8">
|
||||
|
||||
<PathDisplay path={path} updatePath={updatePath} backHome={backHome} backArrow={backArrow}/>
|
||||
<div className="w-2/3 h-5/6 overflow-y-auto border-1 border-gray-300">
|
||||
<DirectoryList dirs={files} showHidden={showHidden} appendPath={appendPath}
|
||||
toggleSelected={toggleSelected}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
23
frontend/src/pages/Login.jsx
Normal file
23
frontend/src/pages/Login.jsx
Normal file
@ -0,0 +1,23 @@
|
||||
import "../index.css";
|
||||
import LoginForm from "../components/LoginForm.jsx";
|
||||
|
||||
function Login() {
|
||||
/**
|
||||
* This is just for allowing easy changes to the domain, if a change occurs.
|
||||
* @type {string}
|
||||
*/
|
||||
const domain = "files.gophernest.net"
|
||||
|
||||
return <>
|
||||
<div className="min-h-screen h-screen w-full bg-gray-200 flex items-center justify-center">
|
||||
<h2 className="absolute top-0 left-0 font-[550] font-mono italic text-xl m-6">{domain}</h2>
|
||||
<div className="w-2/7 h-fit bg-white border-1 rounded-sm border-gray-300 p-12">
|
||||
<p className="text-gray-400">Please enter your details</p>
|
||||
<h1 className="text-3xl font-semibold mt-2 mb-12"> Welcome Back </h1>
|
||||
<LoginForm/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
export default Login;
|
||||
12
frontend/src/pages/NotFound.jsx
Normal file
12
frontend/src/pages/NotFound.jsx
Normal file
@ -0,0 +1,12 @@
|
||||
import "../index.css"
|
||||
|
||||
export default function NotFound() {
|
||||
return <>
|
||||
<div className="min-h-screen h-screen w-full bg-gray-200 flex items-center justify-center">
|
||||
<div className="w-2/7 h-fit bg-white border-1 rounded-sm border-gray-300 p-12">
|
||||
<h1 className="text-6xl font-semibold mt-2 mb-12 opacity-50 text-gray-400"> 404 </h1>
|
||||
<p className="text-sm text-black"> This page could not be found or does not exist.</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
11
frontend/tailwind.config.js
Normal file
11
frontend/tailwind.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
"./index.html",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
10
frontend/vite.config.js
Normal file
10
frontend/vite.config.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(), tailwindcss(),
|
||||
],
|
||||
})
|
||||
Reference in New Issue
Block a user