uploading files and a lot more
This commit is contained in:
@@ -1,12 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Nav />
|
||||
WOAH IS THAT FRICKING SCOTT THE WOZ DUDE
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
266
ui/pages/home/[...name].vue
Normal file
266
ui/pages/home/[...name].vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<script lang="ts" setup>
|
||||
import { useUser } from '~/composables/useUser'
|
||||
import type { File } from '~/types/file';
|
||||
import type { FileUpload } from '~/types/user';
|
||||
const { getUser } = useUser()
|
||||
|
||||
definePageMeta({
|
||||
middleware: "auth"
|
||||
});
|
||||
|
||||
const user = await getUser()
|
||||
|
||||
let { data: usageBytes } = await useFetch<{ usage: number }>('/api/user/usage')
|
||||
let { data: files } = await useFetch<[File]>('/api/files')
|
||||
|
||||
const route = useRoute();
|
||||
let folder = ref("");
|
||||
let uploadPaneClosed = ref(true);
|
||||
|
||||
if (typeof route.params.name == "object") {
|
||||
folder.value = route.params.name.join("/");
|
||||
}
|
||||
|
||||
let recentFiles = ref([]);
|
||||
|
||||
const fileInput: Ref<HTMLInputElement | null> = ref(null);
|
||||
|
||||
const uploadingFiles: Ref<Array<FileUpload>> = ref([]);
|
||||
|
||||
const handleFileChange = (event: Event) => {
|
||||
const files = (<HTMLInputElement>event.target).files;
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
uploadFile(files[i])
|
||||
}
|
||||
|
||||
if (!fileInput.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (fileInput.value.files.length > 0) {
|
||||
fileInput.value.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
const uploadFile = (file: File) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const startTime = Date.now();
|
||||
let id = `${file.name}-${Math.floor(Math.random() * 1000)}`;
|
||||
|
||||
let uploading_file: FileUpload = {
|
||||
id,
|
||||
uploading: true,
|
||||
controller: xhr,
|
||||
startTime,
|
||||
file: file,
|
||||
length: {},
|
||||
status: {}
|
||||
}
|
||||
|
||||
uploadingFiles.value.push(uploading_file)
|
||||
|
||||
if (uploadPaneClosed.value === true) {
|
||||
uploadPaneClosed.value = false;
|
||||
}
|
||||
|
||||
xhr.open('POST', '/api/upload', true);
|
||||
|
||||
xhr.upload.onprogress = (event) => {
|
||||
if (event.lengthComputable) {
|
||||
let file = uploadingFiles.value.find(upload => upload.id === id);
|
||||
if (!file) {
|
||||
throw new Error("Upload is progressing but file is missing!")
|
||||
}
|
||||
|
||||
|
||||
const currentTime = Date.now();
|
||||
const timeElapsed = (currentTime - file.startTime) / 1000;
|
||||
|
||||
file.length = { loaded: event.loaded, total: event.total };
|
||||
|
||||
const uploadedBytes = event.loaded;
|
||||
const totalBytes = event.total;
|
||||
const uploadSpeed = uploadedBytes / timeElapsed;
|
||||
const remainingBytes = totalBytes - uploadedBytes;
|
||||
const remainingTime = remainingBytes / uploadSpeed;
|
||||
|
||||
file.speed = uploadSpeed;
|
||||
file.remainingTime = remainingTime;
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
let data = JSON.parse(xhr.response)
|
||||
usageBytes.value.usage = data.usage
|
||||
files.value?.push(data.file)
|
||||
|
||||
let file = uploadingFiles.value.find(upload => upload.id === id);
|
||||
if (!file) {
|
||||
throw new Error("Upload has finished but file is missing!")
|
||||
}
|
||||
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
file.uploading = false;
|
||||
|
||||
file.status = {
|
||||
error: false,
|
||||
aborted: false,
|
||||
code: xhr.status,
|
||||
message: xhr.statusText
|
||||
};
|
||||
} else {
|
||||
file.uploading = false;
|
||||
|
||||
file.status = {
|
||||
error: true,
|
||||
aborted: false,
|
||||
code: xhr.status,
|
||||
message: xhr.statusText
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
let file = uploadingFiles.value.find(upload => upload.id === id);
|
||||
if (!file) {
|
||||
throw new Error("Upload has errored but file is missing!")
|
||||
}
|
||||
|
||||
file.uploading = false;
|
||||
|
||||
file.status = {
|
||||
error: true,
|
||||
aborted: false,
|
||||
code: xhr.status,
|
||||
message: xhr.statusText
|
||||
};
|
||||
};
|
||||
|
||||
xhr.onabort = () => {
|
||||
let file = uploadingFiles.value.find(upload => upload.id === id);
|
||||
if (!file) {
|
||||
throw new Error("Upload has been aborted but file is missing!")
|
||||
}
|
||||
|
||||
file.uploading = false;
|
||||
|
||||
file.status = {
|
||||
error: true,
|
||||
aborted: true,
|
||||
code: 0,
|
||||
message: "aborted"
|
||||
};
|
||||
};
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
xhr.send(formData);
|
||||
};
|
||||
|
||||
const openFilePicker = () => {
|
||||
fileInput.value?.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex relative min-h-[100dvh]">
|
||||
<div class="fixed md:relative -translate-x-full md:translate-x-0">
|
||||
<FileNav :usageBytes="usageBytes?.usage" />
|
||||
</div>
|
||||
<UploadPane :closed="uploadPaneClosed" v-on:update:closed="(newValue) => uploadPaneClosed = newValue"
|
||||
:uploadingFiles="uploadingFiles" />
|
||||
<div class="w-full">
|
||||
<Nav />
|
||||
<div class="pt-6 pl-12 overflow-auto max-h-[calc(100vh-var(--nav-height))]">
|
||||
<div class="flex gap-x-4 flex-col">
|
||||
<div class="py-5 flex flex-row gap-x-4">
|
||||
<input type="file" ref="fileInput" @change="handleFileChange" multiple class="hidden" />
|
||||
<button v-on:click="openFilePicker"
|
||||
class="rounded-xl border-2 border-surface flex flex-col gap-y-2 px-2 py-3 w-40 justify-center items-center hover:bg-muted/10 active:bg-muted/20 transition-bg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M7 18a4.6 4.4 0 0 1 0-9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-1" />
|
||||
<path d="m9 15l3-3l3 3m-3-3v9" />
|
||||
</g>
|
||||
</svg>
|
||||
Upload
|
||||
</button>
|
||||
<button
|
||||
class="rounded-xl border-2 border-surface flex flex-col gap-y-2 px-2 py-3 w-40 justify-center items-center hover:bg-muted/10 active:bg-muted/20 transition-bg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 19H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h4l3 3h7a2 2 0 0 1 2 2v3.5M16 19h6m-3-3v6" />
|
||||
</svg>
|
||||
New folder
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="recentFiles.length > 0">
|
||||
<h2 class="font-semibold text-2xl">Recent</h2>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-xl">
|
||||
<Breadcrumbs :path="route.path" />
|
||||
</h3>
|
||||
<table class="w-full text-sm mt-2">
|
||||
<thead class="border-b">
|
||||
<tr class="flex flex-row h-10 group pl-[30px] -ml-7 relative items-center">
|
||||
<th class="left-0 absolute">
|
||||
<div>
|
||||
<input class="w-4 h-4 hidden group-hover:block" type="checkbox" />
|
||||
</div>
|
||||
</th>
|
||||
<th class="flex-grow text-start">
|
||||
Name
|
||||
</th>
|
||||
<th class="min-w-40 text-start">
|
||||
Size
|
||||
</th>
|
||||
<th class="min-w-40 text-start sm:block hidden">
|
||||
Last modified
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="block">
|
||||
<tr class="flex flex-row h-10 group items-center border-b hover:bg-muted/10 transition-bg"
|
||||
v-for="file in files">
|
||||
<td class="-ml-7 pr-3.5">
|
||||
<div class="w-4 h-4">
|
||||
<input class="w-4 h-4 hidden group-hover:block" type="checkbox" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="flex-grow text-start">
|
||||
<div class="flex items-center">
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
viewBox="0 0 24 24">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4" />
|
||||
<path
|
||||
d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2M9 9h1m-1 4h6m-6 4h6" />
|
||||
</g>
|
||||
</svg>
|
||||
{{ file.name }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="min-w-40 text-start">
|
||||
{{ formatBytes(file.size) }}
|
||||
</td>
|
||||
<td class="min-w-40 text-start sm:block hidden">
|
||||
{{ file.last_modified }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,10 +1,193 @@
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
middleware: "unauth"
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
OMG IS THAT SCOTT THE WOZ?!?!?!?!?!?!??!?!??!?!?!?!?!?!?
|
||||
<div class="flex flex-col min-h-[100dvh]">
|
||||
<Nav />
|
||||
<main class="flex-1">
|
||||
<section
|
||||
class="flex justify-center w-full py-12 md:py-24 lg:py-32 xl:py-48 min-h-[calc(100dvh-var(--nav-height))]">
|
||||
<div class="container px-4 md:px-6 h-fit">
|
||||
<div class="grid gap-6 lg:grid-cols-[1fr_400px] lg:gap-12 xl:grid-cols-[1fr_600px]">
|
||||
<img src="/placeholder.svg" width="550" height="310" alt="Hero"
|
||||
class="mx-auto aspect-video overflow-hidden rounded-xl object-cover sm:w-full lg:order-last" />
|
||||
<div class="flex flex-col justify-center space-y-4">
|
||||
<div class="space-y-2">
|
||||
<h1 class="text-3xl font-bold tracking-tight sm:text-5xl xl:text-6xl/none">
|
||||
Effortless Open Source File Hosting
|
||||
</h1>
|
||||
<p class="max-w-[600px] text-subtle md:text-xl">
|
||||
Store, share, and access your files from anywhere with our powerful file hosting
|
||||
platform.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 min-[400px]:flex-row">
|
||||
<NuxtLink to="/signup"
|
||||
class="inline-flex h-10 items-center justify-center rounded-md boder px-8 text-sm font-medium transition-bg border-love/40 bg-love/10 text-love hover:bg-love/15 active:bg-love/25 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring">
|
||||
Sign Up
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/login"
|
||||
class="inline-flex h-10 items-center justify-center rounded-md border px-8 text-sm font-medium transition-bg hover:bg-muted/10 active:bg-muted/15 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring">
|
||||
Log In
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="flex justify-center w-full py-12 md:py-24 lg:py-32">
|
||||
<div class="container px-4 md:px-6">
|
||||
<div class="flex flex-col items-center justify-center space-y-4 text-center">
|
||||
<div class="space-y-2">
|
||||
<div class="inline-block rounded-lg bg-subtle/20 px-3 py-1 text-sm">
|
||||
Secure File Hosting
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold tracking-tighter sm:text-5xl">Store Your Files Safely</h2>
|
||||
<p
|
||||
class="max-w-[900px] text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
|
||||
Our platform uses industry-leading encryption to keep your files secure and protected.
|
||||
Access your
|
||||
data from any device, anywhere.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-auto grid max-w-5xl items-center gap-6 py-12 lg:grid-cols-2 lg:gap-12">
|
||||
<img src="/placeholder.svg" width="550" height="310" alt="Image"
|
||||
class="mx-auto aspect-video overflow-hidden rounded-xl object-cover object-center sm:w-full lg:order-last" />
|
||||
<div class="flex flex-col justify-center space-y-4">
|
||||
<ul class="grid gap-6">
|
||||
<li>
|
||||
<div class="grid gap-1">
|
||||
<h3 class="text-xl font-bold">Secure Storage</h3>
|
||||
<p class="text-muted-foreground">
|
||||
Your files are encrypted and stored in a secure environment.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="grid gap-1">
|
||||
<h3 class="text-xl font-bold">Easy Sharing</h3>
|
||||
<p class="text-muted-foreground">
|
||||
Share files with your team or clients with just a few clicks.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="grid gap-1">
|
||||
<h3 class="text-xl font-bold">Cross-Device Access</h3>
|
||||
<p class="text-muted-foreground">Access your files from any device, anywhere
|
||||
in the world.</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="flex justify-center w-full py-12 md:py-24 lg:py-32 bg-subtle/10">
|
||||
<div class="container px-4 md:px-6">
|
||||
<div class="flex flex-col items-center justify-center space-y-4 text-center">
|
||||
<div class="space-y-2">
|
||||
<div class="inline-block rounded-lg bg-subtle/20 px-3 py-1 text-sm">
|
||||
File Management
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold tracking-tighter sm:text-5xl">Effortless File Management</h2>
|
||||
<p
|
||||
class="max-w-[900px] text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
|
||||
Our intuitive file browser makes it easy to upload, download, and manage your files.
|
||||
Organize your data with folders and tags, and share files with your team or clients.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-auto grid max-w-5xl items-center gap-6 py-12 lg:grid-cols-2 lg:gap-12">
|
||||
<img src="/placeholder.svg" width="550" height="310" alt="Image"
|
||||
class="mx-auto aspect-video overflow-hidden rounded-xl object-cover object-center sm:w-full lg:order-last" />
|
||||
<div class="flex flex-col justify-center space-y-4">
|
||||
<ul class="grid gap-6">
|
||||
<li>
|
||||
<div class="grid gap-1">
|
||||
<h3 class="text-xl font-bold">Intuitive File Browser</h3>
|
||||
<p class="text-muted-foreground">
|
||||
Easily navigate and manage your files with our user-friendly interface.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="grid gap-1">
|
||||
<h3 class="text-xl font-bold">Folder Organization</h3>
|
||||
<p class="text-muted-foreground">
|
||||
Create and organize your files into folders for better structure.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="grid gap-1">
|
||||
<h3 class="text-xl font-bold">Sharing and Collaboration</h3>
|
||||
<p class="text-muted-foreground">
|
||||
Easily share files with your team or clients
|
||||
and collaborate on projects.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="flex justify-center w-full py-12 md:py-24 lg:py-32">
|
||||
<div class="container px-4 md:px-6">
|
||||
<div class="flex flex-col items-center justify-center space-y-4 text-center">
|
||||
<div class="space-y-2">
|
||||
<div class="inline-block rounded-lg bg-subtle/20 px-3 py-1 text-sm">
|
||||
Storage Plans
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold tracking-tighter sm:text-5xl">Store 10GB For Free</h2>
|
||||
<p
|
||||
class="max-w-[900px] text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
|
||||
Our storage plans give you flexibility to store whatever you need, whether it's
|
||||
presentations and documents, or pictures and vides, there's a plan for you.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-auto grid max-w-5xl items-center gap-6 py-12 lg:grid-cols-2 lg:gap-12">
|
||||
<img src="/placeholder.svg" width="550" height="310" alt="Image"
|
||||
class="mx-auto aspect-video overflow-hidden rounded-xl object-cover object-center sm:w-full lg:order-last" />
|
||||
<div class="flex flex-col justify-center space-y-4">
|
||||
<ul class="grid gap-6">
|
||||
<li>
|
||||
<div class="grid gap-1">
|
||||
<h3 class="text-xl font-bold">Lorem Ipsum</h3>
|
||||
<p class="text-muted-foreground">
|
||||
Lorem ipsum amet dolar sit.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="grid gap-1">
|
||||
<h3 class="text-xl font-bold">Lorem Ipsum</h3>
|
||||
<p class="text-muted-foreground">
|
||||
Lorem ipsum amet dolar sit.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="grid gap-1">
|
||||
<h3 class="text-xl font-bold">Lorem Ipsum</h3>
|
||||
<p class="text-muted-foreground">Lorem ipsum amet dolar sit.</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup>
|
||||
console.log(useCookie("sessionToken").value)
|
||||
<script lang="ts" setup>
|
||||
import type { User } from '~/types/user';
|
||||
const { fetchUser } = useUser()
|
||||
|
||||
if (useCookie("sessionToken").value) {
|
||||
await navigateTo('/')
|
||||
}
|
||||
definePageMeta({
|
||||
middleware: "unauth"
|
||||
});
|
||||
|
||||
let username_or_email = ref('')
|
||||
let password = ref('')
|
||||
@@ -11,7 +12,7 @@ let password = ref('')
|
||||
let error = ref('')
|
||||
|
||||
const submitForm = async () => {
|
||||
let response = await useFetch('/api/login', {
|
||||
let response = await useFetch<User>('/api/login', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
"username_or_email": username_or_email.value,
|
||||
@@ -24,15 +25,15 @@ const submitForm = async () => {
|
||||
error.value = response.error.value.data.message
|
||||
setTimeout(() => error.value = "", 15000)
|
||||
} else {
|
||||
await navigateTo('/')
|
||||
await fetchUser()
|
||||
await navigateTo('/home')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen min-w-screen grid place-content-center bg-base">
|
||||
<div
|
||||
class="flex flex-col text-center bg-surface border border-muted/20 shadow-md px-10 py-8 rounded-2xl min-w-0 max-w-[313px]">
|
||||
<div class="flex flex-col text-center bg-surface border shadow-md px-10 py-8 rounded-2xl min-w-0 max-w-[313px]">
|
||||
<h2 class="font-semibold text-2xl mb-2">Login</h2>
|
||||
<Input v-model="username_or_email" placeholder="Username or Email..." />
|
||||
<Input v-model="password" type="password" placeholder="Password..." />
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup>
|
||||
console.log(useCookie("sessionToken").value)
|
||||
<script lang="ts" setup>
|
||||
import type { User } from '~/types/user'
|
||||
const { fetchUser } = useUser()
|
||||
|
||||
if (useCookie("sessionToken").value) {
|
||||
await navigateTo('/')
|
||||
}
|
||||
definePageMeta({
|
||||
middleware: "unauth"
|
||||
});
|
||||
|
||||
let username = ref('')
|
||||
let email = ref('')
|
||||
@@ -12,7 +13,7 @@ let password = ref('')
|
||||
let error = ref('')
|
||||
|
||||
const submitForm = async () => {
|
||||
const response = await useFetch('/api/signup', {
|
||||
const response = await useFetch<User>('/api/signup', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
"username": username.value,
|
||||
@@ -26,7 +27,8 @@ const submitForm = async () => {
|
||||
error.value = response.error.value.data.message
|
||||
setTimeout(() => error.value = "", 15000)
|
||||
} else {
|
||||
await navigateTo('/')
|
||||
await fetchUser()
|
||||
await navigateTo('/home')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user