Compare commits
16 commits
Author | SHA1 | Date | |
---|---|---|---|
59a33754e2 | |||
0806ae54c2 | |||
d2b2f11baf | |||
253c3f6e4c | |||
34a4c6b2ce | |||
![]() |
2975e6088d | ||
![]() |
7926efa222 | ||
![]() |
9f91514f78 | ||
![]() |
d36c98cb49 | ||
![]() |
a72ebe853f | ||
![]() |
68b13ab5c2 | ||
![]() |
cf9f2181e9 | ||
![]() |
1aedda9cb3 | ||
![]() |
7be62fcbb2 | ||
![]() |
39005e75e0 | ||
![]() |
c45d8f1e64 |
31 changed files with 6789 additions and 82 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -8,6 +8,7 @@
|
|||
node_modules
|
||||
.env
|
||||
*.local
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# Cache
|
||||
.cache
|
||||
|
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "public"]
|
||||
path = public
|
||||
url = ssh://git@git.iwakura.rip:6969/stereo.cat/public.git
|
11
README.md
11
README.md
|
@ -1,3 +1,14 @@
|
|||
# stereo.cat frontend
|
||||
|
||||
written in typescript with qwik
|
||||
|
||||
## running in dev env
|
||||
```
|
||||
git clone https://git.iwakura.rip/stereo.cat/frontend.git
|
||||
git submodule update --init --recursive
|
||||
bun install
|
||||
bun dev
|
||||
```
|
||||
|
||||
## disclaimer
|
||||
All graphic assets belonging to stereo.cat may not be used in unofficial instances, forks or versions of our software. Please replace them if you are hosting our software yourself, they can be found in the ``public`` folder in this repository. More information (like the full license) can be found [here](https://git.iwakura.rip/stereo.cat/public)
|
||||
|
|
12
bun.lock
12
bun.lock
|
@ -4,6 +4,8 @@
|
|||
"": {
|
||||
"name": "my-qwik-empty-starter",
|
||||
"dependencies": {
|
||||
"@types/aos": "^3.0.7",
|
||||
"aos": "^3.0.0-beta.6",
|
||||
"ky": "^1.8.1",
|
||||
"nanostores": "^1.0.1",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
|
@ -245,6 +247,8 @@
|
|||
|
||||
"@trysound/sax": ["@trysound/sax@0.2.0", "", {}, "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="],
|
||||
|
||||
"@types/aos": ["@types/aos@3.0.7", "", {}, "sha512-sEhyFqvKauUJZDbvAB3Pggynrq6g+2PS4XB3tmUr+mDL1gfDJnwslUC4QQ7/l8UD+LWpr3RxZVR/rHoZrLqZVg=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
@ -297,6 +301,8 @@
|
|||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"aos": ["aos@3.0.0-beta.6", "", { "dependencies": { "classlist-polyfill": "^1.2.0", "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1" } }, "sha512-VLWrpq8bfAWcetynVHMMrqdC+89Qq/Ym6UBJbHB4crIwp3RR8uq1dNGgsFzoDl03S43rlVMK+na3r5+oUCZsYw=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
||||
|
@ -345,6 +351,8 @@
|
|||
|
||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
|
||||
"classlist-polyfill": ["classlist-polyfill@1.2.0", "", {}, "sha512-GzIjNdcEtH4ieA2S8NmrSxv7DfEV5fmixQeyTmqmRmRJPGpRBaSnA2a0VrCjyT8iW8JjEdMbKzDotAJf+ajgaQ=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
|
||||
|
@ -655,8 +663,12 @@
|
|||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="],
|
||||
|
||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
|
6069
package-lock.json
generated
Normal file
6069
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -42,6 +42,8 @@
|
|||
"vite-tsconfig-paths": "^4.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/aos": "^3.0.7",
|
||||
"aos": "^3.0.0-beta.6",
|
||||
"ky": "^1.8.1",
|
||||
"nanostores": "^1.0.1",
|
||||
"tailwind-scrollbar": "^4.0.2"
|
||||
|
|
1
public
Submodule
1
public
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit a4f5725fbaf2db1053be198d81d08e1ea0c3e843
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 500 500"><g clip-path="url(#a)"><circle cx="250" cy="250" r="250" fill="#fff"/><path fill="#18B6F6" d="m367.87 418.45-61.17-61.18-.94.13v-.67L175.7 227.53l32.05-31.13L188.9 87.73 99.56 199.09c-15.22 15.42-18.03 40.51-7.08 59.03l55.83 93.11a46.82 46.82 0 0 0 40.73 22.81l27.65-.27 151.18 44.68Z"/><path fill="#AC7EF4" d="m401.25 196.94-12.29-22.81-6.41-11.67-2.54-4.56-.26.26-33.66-58.63a47.07 47.07 0 0 0-41.27-23.75l-29.51.8-88.01.28a47.07 47.07 0 0 0-40.33 23.34L93.4 207l95.76-119.54L314.7 226.19l-22.3 22.67 13.35 108.54.13-.26v.26h-.26l.26.27 10.42 10.2 50.62 49.78c2.13 2 5.6-.4 4.13-2.96l-31.25-61.85 54.5-101.3 1.73-2c.67-.81 1.33-1.62 1.87-2.42a46.8 46.8 0 0 0 3.34-50.18Z"/><path fill="#fff" d="M315.1 225.65 189.18 87.6l17.9 108.14L175 227l130.5 130.27-11.75-108.14 21.37-23.48Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h500v500H0z"/></clipPath></defs></svg>
|
Before Width: | Height: | Size: 947 B |
|
@ -1,3 +0,0 @@
|
|||
<svg width="281" height="248" viewBox="0 0 281 248" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.744835 19.1645C0.340705 16.0258 0.876443 12.8377 2.2843 10.0034L6.37392 1.7703C7.01957 0.470487 8.7912 0.269553 9.71155 1.39175L85.375 93.6495H195.875L271.538 1.39175C272.459 0.269551 274.23 0.470487 274.876 1.77029L278.966 10.0034C280.374 12.8377 280.909 16.0258 280.505 19.1645L264.378 144.419C256.8 203.277 206.688 247.35 147.344 247.35H133.906C74.5619 247.35 24.4504 203.277 16.872 144.419L0.744835 19.1645Z" fill="#D9D9D9"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 548 B |
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/web-manifest-combined.json",
|
||||
"name": "qwik-project-name",
|
||||
"short_name": "Welcome to Qwik",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#fff",
|
||||
"description": "A Qwik project app."
|
||||
}
|
|
@ -3,14 +3,14 @@ import { useNanostore$ } from "~/hooks/nanostores";
|
|||
import { api } from "~/lib/api";
|
||||
import { areFilesLoaded, dashboardFiles } from "~/lib/stores";
|
||||
import { StereoFile } from "~/lib/types";
|
||||
import { SolarUploadLinear, SvgSpinnersBarsRotateFade } from "./Icons";
|
||||
import StereoLogo from "./StereoLogo";
|
||||
import { SolarUploadLinear, SvgSpinnersBarsRotateFade } from "../misc/Icons";
|
||||
import StereoLogo from "../misc/StereoLogo";
|
||||
|
||||
export default component$(() => {
|
||||
const loaded = useNanostore$<boolean>(areFilesLoaded);
|
||||
const files = useNanostore$<StereoFile[]>(dashboardFiles);
|
||||
const fileInputRef = useSignal<HTMLInputElement>();
|
||||
const uploadingFile = useSignal<NoSerialize<File> | undefined>();
|
||||
const uploadingFiles = useSignal<NoSerialize<File[]> | undefined>();
|
||||
const now = useSignal(new Date());
|
||||
|
||||
useVisibleTask$(() => {
|
||||
|
@ -20,33 +20,38 @@ export default component$(() => {
|
|||
return () => clearInterval(interval);
|
||||
});
|
||||
|
||||
const uploadFile = $(async () => {
|
||||
if (!uploadingFile.value) {
|
||||
console.error("No file selected for upload.");
|
||||
const uploadFiles = $(async () => {
|
||||
if (!uploadingFiles.value) {
|
||||
console.error("No file(s) selected for upload.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const unsafe = uploadingFile.value as File;
|
||||
const name = unsafe.name.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
||||
const f = new File([unsafe], name, { type: unsafe.type });
|
||||
const ufiles = uploadingFiles.value as File[];
|
||||
|
||||
for (const file of ufiles) {
|
||||
const name = file.name.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
||||
const f = new File([file], name, { type: file.type });
|
||||
|
||||
await api.upload(f);
|
||||
}
|
||||
|
||||
await api.upload(f);
|
||||
files.value = await api.list();
|
||||
} catch (error) {
|
||||
console.error("Error uploading file:", error);
|
||||
}
|
||||
})
|
||||
return (
|
||||
<div class="z-[999999999] fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-neutral-700/10 backdrop-blur-3xl w-1/3 p-2 pr-4 rounded-lg flex items-center justify-between">
|
||||
<div class="z-[999999999] fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-neutral-700/10 backdrop-blur-3xl lg:w-1/3 md:w-2/3 w-4/5 p-2 pr-4 rounded-lg flex items-center justify-between">
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style="display: none;"
|
||||
onChange$={async (e: Event) => {
|
||||
uploadingFile.value = noSerialize((e.target as HTMLInputElement).files![0]);
|
||||
await uploadFile();
|
||||
uploadingFiles.value = noSerialize(Object.values((e.target as HTMLInputElement).files || {}));
|
||||
await uploadFiles();
|
||||
}}
|
||||
multiple
|
||||
/>
|
||||
|
||||
<div class="flex items-center gap-2">
|
62
src/components/dashboard/Dropzone.tsx
Normal file
62
src/components/dashboard/Dropzone.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { component$, useVisibleTask$ } from "@builder.io/qwik";
|
||||
import { useDropzone } from "~/hooks/dropzone";
|
||||
import { useNanostore$ } from "~/hooks/nanostores";
|
||||
import { api } from "~/lib/api";
|
||||
import { dashboardFiles } from "~/lib/stores";
|
||||
import { StereoFile } from "~/lib/types";
|
||||
|
||||
export default component$(() => {
|
||||
const dashfiles = useNanostore$<StereoFile[]>(dashboardFiles);
|
||||
|
||||
const {
|
||||
highlight,
|
||||
onDragOver,
|
||||
onDragLeave,
|
||||
onInputChange,
|
||||
triggerFileInput,
|
||||
fileInputRef,
|
||||
} = useDropzone();
|
||||
|
||||
useVisibleTask$(() => {
|
||||
const dropzone = document.getElementById("dropzone");
|
||||
if (!dropzone) return;
|
||||
const handler = async (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
highlight.value = false;
|
||||
const files = Array.from(e.dataTransfer?.files || []);
|
||||
for (const file of files) {
|
||||
const name = file.name.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
||||
const f = new File([file], name, { type: file.type });
|
||||
await api.upload(f);
|
||||
dashfiles.value = await api.list();
|
||||
}
|
||||
};
|
||||
dropzone.addEventListener("drop", handler);
|
||||
return () => dropzone.removeEventListener("drop", handler);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
id="dropzone"
|
||||
preventdefault:drop
|
||||
class={{
|
||||
"relative z-10 border-2 border-dashed rounded-lg p-10 transition-colors": true,
|
||||
"border-white bg-black/20": highlight.value,
|
||||
"border-neutral-800": !highlight.value,
|
||||
}}
|
||||
onDragOver$={onDragOver}
|
||||
onDragLeave$={onDragLeave}
|
||||
onClick$={triggerFileInput}
|
||||
>
|
||||
<p class="text-center text-neutral-500 pointer-events-none">drop file ehre</p>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
multiple
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
onChange$={onInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
|
@ -1,9 +1,9 @@
|
|||
import { $, component$ } from "@builder.io/qwik";
|
||||
import { $, component$, Signal, useSignal, useTask$ } from "@builder.io/qwik";
|
||||
import { useNanostore$ } from "~/hooks/nanostores";
|
||||
import { api } from "~/lib/api";
|
||||
import { dashboardFiles } from "~/lib/stores";
|
||||
import { StereoFile } from "~/lib/types";
|
||||
import { SolarClipboardAddBold, SolarDownloadMinimalisticBold, SolarTrashBin2Bold } from "./Icons";
|
||||
import { SolarClipboardAddBold, SolarDownloadMinimalisticBold, SolarTrashBin2Bold } from "../misc/Icons";
|
||||
|
||||
type FileProps = {
|
||||
file: StereoFile;
|
||||
|
@ -19,9 +19,9 @@ const formatSize = (bytes: number) => {
|
|||
export default component$(({ file }: FileProps) => {
|
||||
const files = useNanostore$<StereoFile[]>(dashboardFiles);
|
||||
|
||||
const deleteFile = $(async (uid: string, name: string) => {
|
||||
const deleteFile = $(async (id: string) => {
|
||||
if (!confirm("Are you sure you want to delete this file?")) return;
|
||||
console.log(await api.delete(uid, name));
|
||||
await api.delete(id);
|
||||
files.value = await api.list();
|
||||
});
|
||||
|
||||
|
@ -65,22 +65,20 @@ export default component$(({ file }: FileProps) => {
|
|||
return (
|
||||
<div class="rounded-xl bg-neutral-900 flex flex-col group overflow-hidden hover:bg-neutral-800 transition-all duration-200">
|
||||
<div class="relative">
|
||||
<a href={`/api/${file.Owner}/${file.Name}`} target="_blank">
|
||||
{ (file.Name.endsWith(".png") || file.Name.endsWith(".jpg") || file.Name.endsWith(".jpeg")) && (
|
||||
<img
|
||||
width={400}
|
||||
height={300}
|
||||
src={`/api/${file.Owner}/${file.Name}`}
|
||||
alt={file.Name}
|
||||
class="w-full h-60 object-cover bg-neutral-800 flex-grow"
|
||||
/>
|
||||
)}
|
||||
<a
|
||||
href={`/api/${file.ID}`}
|
||||
target="_blank"
|
||||
class="flex w-full h-60 overflow-clip"
|
||||
>
|
||||
<div class="flex flex-grow group-hover:scale-105 transition-all duration-500 bg-neutral-800">
|
||||
<FilePreview file={file} />
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="absolute bottom-2 right-2 gap-2 z-10 group-hover:flex hidden duration-200 transition-all">
|
||||
<a
|
||||
class="bg-neutral-600/40 backdrop-blur-lg hover:bg-neutral-600/75 transition-all duration-200 text-white p-2 rounded-lg active:scale-95"
|
||||
href={`/api/${file.Owner}/${file.Name}`}
|
||||
href={`/api/${file.ID}`}
|
||||
target="_blank"
|
||||
>
|
||||
<SolarDownloadMinimalisticBold class="w-6 h-6"/>
|
||||
|
@ -95,7 +93,7 @@ export default component$(({ file }: FileProps) => {
|
|||
|
||||
<button
|
||||
class="bg-red-600/50 backdrop-blur-lg hover:bg-red-600/75 transition-all duration-200 text-white p-2 rounded-lg active:scale-95"
|
||||
onClick$={async () => await deleteFile(file.Owner, file.Name)}
|
||||
onClick$={async () => await deleteFile(file.ID)}
|
||||
>
|
||||
<SolarTrashBin2Bold class="w-6 h-6"/>
|
||||
</button>
|
||||
|
@ -117,3 +115,85 @@ export default component$(({ file }: FileProps) => {
|
|||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const FilePreview = component$(({ file }: FileProps) => {
|
||||
type FileType =
|
||||
| "image"
|
||||
| "video"
|
||||
| "audio"
|
||||
| "other";
|
||||
|
||||
const type: Signal<FileType> = useSignal<FileType>("other");
|
||||
const extension = file.Name.split('.').pop()?.toLowerCase() || "";
|
||||
|
||||
useTask$(async () => {
|
||||
if (
|
||||
["png", "jpg", "jpeg", "gif"]
|
||||
.includes(extension)) type.value = "image";
|
||||
|
||||
else if (
|
||||
["mp4", "webm", "ogg", "avi", "mov", "mkv"]
|
||||
.includes(extension)) type.value = "video";
|
||||
else if (
|
||||
["mp3", "wav", "ogg", "flac", "aac"]
|
||||
.includes(extension)) type.value = "audio";
|
||||
|
||||
else type.value = "other";
|
||||
});
|
||||
|
||||
switch (type.value) {
|
||||
case "image":
|
||||
return (
|
||||
<div class="w-full h-60 object-cover flex-grow relative">
|
||||
<img
|
||||
width={400}
|
||||
height={300}
|
||||
src={`/api/${file.ID}`}
|
||||
alt={file.Name}
|
||||
class="w-full h-60 object-cover flex-grow"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "video":
|
||||
return (
|
||||
<div class="w-full h-60 object-cover flex-grow relative">
|
||||
<video
|
||||
width={400}
|
||||
height={300}
|
||||
src={`/api/${file.ID}`}
|
||||
class="w-full h-60 object-cover flex-grow"
|
||||
controls
|
||||
autoplay
|
||||
muted
|
||||
>
|
||||
<div class="w-full h-60 flex items-center justify-center">
|
||||
<p class="text-white/50 text-lg font-light">
|
||||
Preview not available
|
||||
</p>
|
||||
</div>
|
||||
</video>
|
||||
</div>
|
||||
);
|
||||
case "audio":
|
||||
return (
|
||||
<div class="w-full h-60 flex items-center justify-center p-2">
|
||||
<audio
|
||||
controls
|
||||
class="w-full h-12"
|
||||
src={`/api/${file.ID}`}
|
||||
>
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
);
|
||||
case "other":
|
||||
default:
|
||||
return (
|
||||
<div class="w-full h-60 flex items-center justify-center">
|
||||
<p class="text-white/50 text-lg font-light">
|
||||
Preview not available
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
23
src/components/landing/CallToAction.tsx
Normal file
23
src/components/landing/CallToAction.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { component$ } from "@builder.io/qwik";
|
||||
import { OAUTH_LINK } from "~/lib/constants";
|
||||
|
||||
export default component$(() => (
|
||||
<div
|
||||
data-aos="fade-up"
|
||||
data-aos-duration="1000"
|
||||
class="flex flex-col gap-1.5 items-center justify-center w-4/5 bg-gradient-to-b from-stereo/30 to-transparent rounded-2xl h-96 py-4"
|
||||
>
|
||||
<p class="text-4xl text-center">
|
||||
ready to try the <span class="text-stereo">stereo</span> experience?
|
||||
</p>
|
||||
<p class="text-xl text-white/80 text-center">
|
||||
join over <span class="text-stereo">100k</span> other people hosting their files with <span class="text-stereo">stereo</span>!
|
||||
</p>
|
||||
<a
|
||||
href={OAUTH_LINK}
|
||||
class="px-12 py-1.5 mt-1.5 text-lg font-medium text-white bg-stereo rounded-full hover:text-stereo hover:bg-white transition duration-300"
|
||||
>
|
||||
get started
|
||||
</a>
|
||||
</div>
|
||||
));
|
29
src/components/landing/Footer.tsx
Normal file
29
src/components/landing/Footer.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { component$ } from "@builder.io/qwik";
|
||||
import StereoLogo from "../misc/StereoLogo";
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
<div class="w-screen flex py-16 px-16">
|
||||
<div class="flex flex-col flex-shrink h-full justify-start items-start gap-4">
|
||||
<div class="flex flex-col">
|
||||
<span class="flex gap-[1ch]">
|
||||
<StereoLogo class="w-8 h-8 text-stereo" />
|
||||
<span class="text-white font-medium text-2xl">stereo<span class="text-stereo font-bold">.</span>cat</span>
|
||||
</span>
|
||||
|
||||
<span class="text-white/80 font-light text-lg">
|
||||
store all your precious moments with <span class="text-stereo font-medium">stereo</span>.
|
||||
</span>
|
||||
|
||||
<span class="text-white/80 font-light text-md">
|
||||
copyright © {new Date().getFullYear()} stereo.cat - all rights reserved.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-grow justify-end items-start gap-12">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
83
src/components/landing/Hero.tsx
Normal file
83
src/components/landing/Hero.tsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { component$, useSignal } from "@builder.io/qwik";
|
||||
import { useRelativeMouse } from "~/hooks/mouse";
|
||||
import { OAUTH_LINK } from "~/lib/constants";
|
||||
import GradientBorder from "../misc/GradientBorder";
|
||||
|
||||
export default component$(() => {
|
||||
const ref1 = useSignal<HTMLElement>();
|
||||
const mouse1 = useRelativeMouse(ref1);
|
||||
|
||||
const ref2 = useSignal<HTMLElement>();
|
||||
const mouse2 = useRelativeMouse(ref2);
|
||||
|
||||
return (
|
||||
<div class="flex flex-col w-full bg-gradient-to-b from-stereo/30 to-transparent overflow-x-clip select-none">
|
||||
<div class="mt-62 flex flex-col justify-center w-full h-full text-center">
|
||||
<div class="flex flex-col items-center justify-center gap-2 font-light">
|
||||
<p class="text-6xl">
|
||||
you bring the files, we'll bring the <span class="text-stereo">magic</span>.
|
||||
</p>
|
||||
<p class="text-2xl text-white/80">
|
||||
stereo is no-bs file-host inspired by Tixte, that you'll love to use
|
||||
</p>
|
||||
<div class="flex gap-4 mt-1">
|
||||
<a
|
||||
href={OAUTH_LINK}
|
||||
class="px-6 py-1 text-lg font-medium text-white bg-stereo rounded-full hover:text-stereo hover:bg-white transition duration-300"
|
||||
>
|
||||
get started
|
||||
</a>
|
||||
<a
|
||||
href="discord.gg"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="px-6 py-1 text-lg font-medium text-stereo rounded-full border border-stereo hover:bg-stereo hover:text-white transition duration-300"
|
||||
>
|
||||
learn more
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-10 flex items-center" style={{ perspective: "1000px" }}>
|
||||
<GradientBorder
|
||||
ref={ref1}
|
||||
size="3px"
|
||||
from="#ff264e"
|
||||
to="transparent"
|
||||
direction="to bottom"
|
||||
style={{
|
||||
transformStyle: "preserve-3d",
|
||||
transformOrigin: "center center",
|
||||
transform: `
|
||||
rotateX(${(mouse1.y / 120) + 30}deg)
|
||||
rotateY(${(-mouse1.x / 120) + 25}deg)
|
||||
rotateZ(-10deg)
|
||||
`,
|
||||
}}
|
||||
class="rounded-2xl -mr-12"
|
||||
>
|
||||
<img src="dashboard 1.png" class="h-[32rem] rounded-2xl shadow-2xl shadow-stereo/10" />
|
||||
</GradientBorder>
|
||||
<GradientBorder
|
||||
ref={ref2}
|
||||
size="3px"
|
||||
from="#ff264e"
|
||||
to="transparent"
|
||||
direction="to bottom"
|
||||
style={{
|
||||
transformStyle: "preserve-3d",
|
||||
transformOrigin: "center center",
|
||||
transform: `
|
||||
rotateX(${(mouse2.y / 120) + 30}deg)
|
||||
rotateY(${(-mouse2.x / 60) - 25}deg)
|
||||
rotateZ(10deg)
|
||||
`,
|
||||
}}
|
||||
class="rounded-2xl -ml-12 shadow-2xl shadow-stereo/10"
|
||||
>
|
||||
<img src="dashboard 2.png" class="h-[36rem] rounded-2xl" />
|
||||
</GradientBorder>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
41
src/components/landing/Navbar.tsx
Normal file
41
src/components/landing/Navbar.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { component$ } from "@builder.io/qwik";
|
||||
import StereoLogo from "../misc/StereoLogo";
|
||||
|
||||
export default component$(() => {
|
||||
const items = [
|
||||
{ text: "Synopsis", href: "#" },
|
||||
{ text: "Discord", href: "#" },
|
||||
{ text: "Dashboard", href: "/dashboard", highlighted: true },
|
||||
{ text: "Pricing", href: "#" },
|
||||
{ text: "Terms", href: "#" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
data-aos="fade-down"
|
||||
data-aos-anchor-placement="top-center"
|
||||
data-aos-duration="1000"
|
||||
class="fixed flex items-center justify-start top-6 left-1/2 transform -translate-x-1/2 bg-neutral-950 p-8 h-10 rounded-full lg:w-2/3 md:w-4/5 w-4/5 z-[9999999] shadow-lg">
|
||||
<div class="flex flex-1/3">
|
||||
<StereoLogo class="w-10 h-10 text-stereo hover:text-white transition-all duration-300 hover:cursor-pointer" />
|
||||
</div>
|
||||
<div class="w-full hidden md:flex flex-grow gap-4 items-center justify-center">
|
||||
{items.map(({ text, href, highlighted: h }) => (
|
||||
<a
|
||||
key={text}
|
||||
href={href}
|
||||
class={
|
||||
h ? "px-4 py-0.5 hover:text-stereo hover:bg-white rounded-full font-medium bg-stereo text-white transition duration-300 text-lg"
|
||||
: "text-white font-light hover:text-stereo transition duration-300 text-lg"
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div class="flex flex-1/3 items-center justify-end">
|
||||
<span class="text-white group hover:text-stereo transition duration-300 font-medium text-2xl">stereo<span class="text-stereo group-hover:text-white transition duration-300 font-bold">.</span>cat</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
});
|
40
src/components/landing/Stats.tsx
Normal file
40
src/components/landing/Stats.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { component$ } from "@builder.io/qwik";
|
||||
|
||||
export default component$(() => {
|
||||
type StatProps = {
|
||||
stat: string,
|
||||
description: string;
|
||||
}
|
||||
|
||||
const Stat = component$(({ stat, description }: StatProps) => (
|
||||
<div class="group hover:scale-105 transition-all duration-300 bg-gradient-to-t from-stereo/30 to-transparent p-8 px-12 rounded-2xl h-96 flex flex-col items-center text-center shadow-2xl shadow-stereo/15">
|
||||
<div class="flex-grow flex items-center justify-center">
|
||||
<p class="text-7xl font-bold text-stereo group-hover:text-white transition-colours duration-300">
|
||||
{stat.includes("+") ? stat.substring(0, stat.length - 1) : stat}
|
||||
{stat.endsWith("+") ? <span class="text-stereo/80 text-4xl align-super">+</span> : ""}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-xl w-3/4 text-wrap text-white/30 group-hover:text-white/60 transition-colours duration-300">{description}</p>
|
||||
</div>
|
||||
) );
|
||||
|
||||
return (
|
||||
<div
|
||||
data-aos="fade-up"
|
||||
data-aos-duration="1000"
|
||||
class="flex flex-col gap-12 items-center justify-center w-full"
|
||||
>
|
||||
<div class="flex flex-col gap-1 items-center justify-center">
|
||||
<p class="text-lg text-stereo font-bold uppercase">statistics</p>
|
||||
<p class="text-2xl text-white/80">
|
||||
we know what you're thinking, and we have the numbers to prove it.
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Stat stat="1.5M+" description="files hosted on stereo" />
|
||||
<Stat stat="100k+" description="active users on stereo" />
|
||||
<Stat stat="99.9%" description="uptime guarantee for all files" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
});
|
112
src/components/landing/Testimonials.tsx
Normal file
112
src/components/landing/Testimonials.tsx
Normal file
|
@ -0,0 +1,112 @@
|
|||
import { $, component$, useOnDocument, useSignal } from "@builder.io/qwik";
|
||||
import ky from "ky";
|
||||
|
||||
export default component$(() => {
|
||||
type TestimonialProps = {
|
||||
nickname: string;
|
||||
pfp?: string;
|
||||
id?: string;
|
||||
quote: string;
|
||||
};
|
||||
|
||||
type LanyardResponse = {
|
||||
data: {
|
||||
discord_status: "online" | "idle" | "dnd";
|
||||
discord_user: {
|
||||
avatar: string;
|
||||
}
|
||||
},
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
const Testimonial = component$(({ nickname, id, quote, pfp }: TestimonialProps) => {
|
||||
const lanyard = useSignal<LanyardResponse>();
|
||||
|
||||
useOnDocument("DOMContentLoaded", $(async () => {
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
const response = await ky.get(`https://api.lanyard.rest/v1/users/${id}`).json<LanyardResponse>();
|
||||
lanyard.value = response;
|
||||
console.log("Lanyard data:", lanyard.value);
|
||||
} catch (error) {
|
||||
console.error("Error fetching lanyard data:", error);
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<div
|
||||
data-aos="fade-up"
|
||||
data-aos-duration="1000"
|
||||
class="flex gap-6 items-center bg-gradient-to-t from-stereo/15 to-stereo/5 rounded-2xl p-6 max-w-2xl shadow-2xl shadow-stereo/15"
|
||||
>
|
||||
<div class="relative h-30 aspect-square flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
border: lanyard.value ? `3px solid ${
|
||||
lanyard.value.data.discord_status === "online"
|
||||
? "#43b581"
|
||||
: lanyard.value.data.discord_status === "idle"
|
||||
? "#faa61a"
|
||||
: lanyard.value.data.discord_status === "dnd"
|
||||
? "#f04747"
|
||||
: "#7289da"
|
||||
}` : "",
|
||||
padding: lanyard.value ? `3px` : "0px",
|
||||
}}>
|
||||
<img
|
||||
src={pfp ? pfp : (
|
||||
lanyard.value
|
||||
? `https://cdn.discordapp.com/avatars/${id}/${lanyard.value.data.discord_user.avatar}.png`
|
||||
: `https://api.dicebear.com/9.x/shapes/svg?seed=${Math.random().toString(36).substring(2, 15)}.png`
|
||||
)}
|
||||
class="rounded-full h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-stereo text-9xl h-min -mb-18">“</p>
|
||||
<p class="text-lg text-white/90">
|
||||
{quote}
|
||||
</p>
|
||||
<p class="text-white/60 font-light italic">— {nickname}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
data-aos="fade-up"
|
||||
data-aos-duration="1000"
|
||||
class="flex flex-col gap-6 items-center justify-center w-4/5"
|
||||
>
|
||||
<div class="flex flex-col gap-1 items-center justify-center">
|
||||
<p class="text-lg text-stereo font-bold uppercase">testimonials</p>
|
||||
<p class="text-2xl text-white/80">
|
||||
don't just take our word for it, hear it from our users.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Testimonial
|
||||
nickname="grng"
|
||||
id="829372486780715018"
|
||||
quote="stereo is the best file host I've ever used, it's fast, reliable, and the interface is so clean and easy to use. I love it!"
|
||||
/>
|
||||
|
||||
<Testimonial
|
||||
nickname="hexlocation"
|
||||
id="1325924978805440522"
|
||||
quote="I've been using stereo for a while now, and I can't imagine going back to any other file host. It's just that good!"
|
||||
/>
|
||||
|
||||
<Testimonial
|
||||
nickname="an anonymous user"
|
||||
quote="stereo has changed the way I share files, it's so easy to use and the performance is top-notch. Highly recommend!"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xl text-white/50">
|
||||
and many, many more...
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
});
|
40
src/components/misc/GradientBorder.tsx
Normal file
40
src/components/misc/GradientBorder.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { component$, CSSProperties, QwikIntrinsicElements, Slot } from "@builder.io/qwik";
|
||||
|
||||
type GradientProps = {
|
||||
size: string;
|
||||
from: string;
|
||||
to: string;
|
||||
direction?: string;
|
||||
};
|
||||
|
||||
export default component$((props: GradientProps & QwikIntrinsicElements["div"]) => {
|
||||
const {
|
||||
size,
|
||||
from,
|
||||
to,
|
||||
direction,
|
||||
style: userStyle,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const borderStyle: CSSProperties = {
|
||||
padding: size,
|
||||
background: `linear-gradient(${direction || "to bottom"}, ${from}, ${to})`,
|
||||
display: "inline-block",
|
||||
};
|
||||
|
||||
// Only spread userStyle if it's an object
|
||||
const mergedStyle =
|
||||
userStyle && typeof userStyle === "object"
|
||||
? { ...borderStyle, ...userStyle }
|
||||
: borderStyle;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={mergedStyle}
|
||||
{...rest}
|
||||
>
|
||||
<Slot />
|
||||
</div>
|
||||
);
|
||||
});
|
|
@ -3,6 +3,5 @@
|
|||
|
||||
@theme {
|
||||
--font-sans: 'DM Sans', sans-serif;
|
||||
--color-stereo: #ff264e;
|
||||
}
|
||||
|
||||
@plugin 'tailwind-scrollbar';
|
28
src/hooks/dropzone.ts
Normal file
28
src/hooks/dropzone.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { $, useSignal } from '@builder.io/qwik';
|
||||
|
||||
export const useDropzone = () => {
|
||||
const highlight = useSignal(false);
|
||||
|
||||
const onInputChange = $(async (e: Event) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
const fileInputRef = useSignal<HTMLInputElement | undefined>(undefined);
|
||||
|
||||
return {
|
||||
highlight,
|
||||
onDragOver: $((e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
highlight.value = true;
|
||||
}),
|
||||
onDragLeave: $((e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
highlight.value = false;
|
||||
}),
|
||||
onInputChange,
|
||||
triggerFileInput: $(() => {
|
||||
fileInputRef.value?.click();
|
||||
}),
|
||||
fileInputRef,
|
||||
};
|
||||
}
|
27
src/hooks/mouse.ts
Normal file
27
src/hooks/mouse.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { $, Signal, useOnDocument, useStore } from "@builder.io/qwik";
|
||||
|
||||
export const useMouse = () => {
|
||||
const pos = useStore({ x: 0, y: 0 });
|
||||
|
||||
useOnDocument("mousemove", $((event: MouseEvent) => {
|
||||
const { clientX, clientY } = event;
|
||||
pos.x = clientX;
|
||||
pos.y = clientY;
|
||||
}));
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
export const useRelativeMouse = (ref: Signal<HTMLElement | undefined>) => {
|
||||
const pos = useStore({ x: 0, y: 0 });
|
||||
|
||||
useOnDocument("mousemove", $((event: MouseEvent) => {
|
||||
const el = ref.value;
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
pos.x = event.clientX - rect.left;
|
||||
pos.y = event.clientY - rect.top;
|
||||
}));
|
||||
|
||||
return pos;
|
||||
};
|
|
@ -5,16 +5,14 @@ export const client = ky.create({
|
|||
prefixUrl: '/api',
|
||||
credentials: 'include'
|
||||
});
|
||||
// TODO: make wrapper for apiclient fr
|
||||
|
||||
export const api = {
|
||||
file: async (file_id: string) => await client.get(file_id),
|
||||
file: async (uid: string) => await client.get<Blob>(uid),
|
||||
list: async () => await client.get('list').json<StereoFile[]>(),
|
||||
upload: async (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return await client.post('upload', { body: formData });
|
||||
},
|
||||
delete: async (uid: string, file: string) => {
|
||||
return await client.delete(`${uid}/${file}`).json();
|
||||
},
|
||||
delete: async (uid: string) => await client.delete(uid).json(),
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
export type StereoFile = {
|
||||
ID: string;
|
||||
Name: string;
|
||||
Owner: string;
|
||||
CreatedAt: string;
|
||||
Size: number;
|
||||
CreatedAt: string;
|
||||
Mime: string;
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import { $, component$, useOnDocument } from "@builder.io/qwik";
|
||||
import { component$, useVisibleTask$ } from "@builder.io/qwik";
|
||||
import { routeLoader$, type DocumentHead } from "@builder.io/qwik-city";
|
||||
import Controlbar from "~/components/Controlbar";
|
||||
import File from "~/components/File";
|
||||
import { SolarUploadLinear, SvgSpinnersBarsRotateFade } from "~/components/Icons";
|
||||
import Controlbar from "~/components/dashboard/Controlbar";
|
||||
// import Dropzone from "~/components/Dropzone";
|
||||
import File from "~/components/dashboard/File";
|
||||
import { SolarUploadLinear, SvgSpinnersBarsRotateFade } from "~/components/misc/Icons";
|
||||
import { useNanostore$ } from "~/hooks/nanostores";
|
||||
import { api } from "~/lib/api";
|
||||
import { areFilesLoaded, dashboardFiles } from "~/lib/stores";
|
||||
|
@ -11,7 +12,6 @@ import { StereoFile } from "~/lib/types";
|
|||
export const useAuthCheck = routeLoader$(({ cookie, redirect: r }) => {
|
||||
const jwt = cookie.get("jwt");
|
||||
if (jwt) return {};
|
||||
|
||||
throw r(302, "/");
|
||||
});
|
||||
|
||||
|
@ -19,32 +19,42 @@ export default component$(() => {
|
|||
const files = useNanostore$<StereoFile[]>(dashboardFiles);
|
||||
const loaded = useNanostore$<boolean>(areFilesLoaded);
|
||||
|
||||
useOnDocument("DOMContentLoaded", $(async () => {
|
||||
useVisibleTask$(async () => {
|
||||
loaded.value = false;
|
||||
files.value = await api.list();
|
||||
console.log("Files loaded:", files.value);
|
||||
loaded.value = true;
|
||||
}));
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <Dropzone /> */}
|
||||
<Controlbar />
|
||||
{!loaded.value ? (
|
||||
<div class="absolute w-full h-screen flex justify-center items-center flex-col">
|
||||
<p class="text-gray-500 text-8xl font-bold"><SvgSpinnersBarsRotateFade /></p>
|
||||
<p class="text-gray-700 text-2xl font-light italic">loading your files...</p>
|
||||
<span class="text-gray-700 text-lg font-light flex gap-[0.5ch] items-center">please wait <span class="animate-spin">⏳</span></span>
|
||||
<span class="text-gray-700 text-lg font-light flex gap-[0.5ch] items-center">please wait... <span class="animate-spin">⏳</span></span>
|
||||
</div>
|
||||
) : (
|
||||
files.value.length === 0 ? (
|
||||
<div class="absolute w-full h-screen flex justify-center items-center flex-col">
|
||||
<p class="text-gray-500 text-8xl font-bold">{"┻━┻︵ \\(°□°)/ ︵ ┻━┻"}</p>
|
||||
<p class="text-gray-500 text-8xl font-bold">{
|
||||
[
|
||||
"┻━┻︵ \\(°□°)/ ︵ ┻━┻",
|
||||
"┻━┻︵ヽ(`Д´)ノ︵ ┻━┻",
|
||||
"ʕノ•ᴥ•ʔノ ︵ ┻━┻",
|
||||
"(╯°Д°)╯︵ /(.□ . \\)",
|
||||
"┬─┬ ︵ /(.□. \\)",
|
||||
"(/ .□.)\\ ︵╰(゜Д゜)╯︵ /(.□. \\)"
|
||||
].sort(() => Math.random() - 0.5)[0]
|
||||
}</p>
|
||||
<p class="text-gray-700 text-2xl font-light italic">you haven't uploaded any files yet!</p>
|
||||
<span class="text-gray-700 text-lg font-light flex gap-[0.5ch] items-center">click the <span><SolarUploadLinear /></span> button to get started</span>
|
||||
<span class="text-gray-700 text-lg font-light flex gap-[0.5ch] items-center">click the <span class="animate-bounce p-0.5 bg-gray-500 rounded-sm"><SolarUploadLinear /></span> button to get started</span>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div class="grid grid-cols-4 gap-4 p-4 mb-18">
|
||||
<div class="grid xl:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-3 p-6 mb-14">
|
||||
{files.value.map((file) => (
|
||||
<File key={file.Name} file={file} />
|
||||
))}
|
||||
|
|
|
@ -1,18 +1,44 @@
|
|||
/* eslint-disable qwik/jsx-img */
|
||||
import { component$ } from "@builder.io/qwik";
|
||||
import { routeLoader$ } from "@builder.io/qwik-city";
|
||||
import { OAUTH_LINK } from "~/lib/constants";
|
||||
import { DocumentHead, routeLoader$ } from "@builder.io/qwik-city";
|
||||
import CallToAction from "~/components/landing/CallToAction";
|
||||
import Footer from "~/components/landing/Footer";
|
||||
import Hero from "~/components/landing/Hero";
|
||||
import Navbar from "~/components/landing/Navbar";
|
||||
import Stats from "~/components/landing/Stats";
|
||||
import Testimonials from "~/components/landing/Testimonials";
|
||||
|
||||
export const useAuthCheck = routeLoader$(({ cookie, redirect: r }) => {
|
||||
const jwt = cookie.get("jwt");
|
||||
if (!jwt) return {};
|
||||
export const useAuthCheck = routeLoader$(({ cookie, redirect: r, query }) => {
|
||||
const jwt = cookie.get("jwt");
|
||||
const set = Boolean(query.get("jwt_set"));
|
||||
|
||||
throw r(302, "/dashboard");
|
||||
if (jwt && set) {
|
||||
throw r(302, "/dashboard");
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
<div class="flex flex-col items-center justify-center flex-grow">
|
||||
<a href={OAUTH_LINK} class="bg-white text-black p-2 rounded-lg">clik heir 2 auth!!!!!!</a>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div class="flex flex-col flex-grow w-screen items-center mb-24 gap-56">
|
||||
<Hero />
|
||||
<Stats />
|
||||
<Testimonials />
|
||||
<CallToAction />
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export const head: DocumentHead = {
|
||||
title: "Welcome to Qwik",
|
||||
meta: [
|
||||
{
|
||||
name: "description",
|
||||
content: "Qwik site description",
|
||||
},
|
||||
],
|
||||
};
|
|
@ -1,9 +1,25 @@
|
|||
import { component$, Slot } from '@builder.io/qwik';
|
||||
import { $, component$, Slot, useOnDocument } from '@builder.io/qwik';
|
||||
import AOS from 'aos';
|
||||
import 'aos/dist/aos.css';
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
<div class="flex flex-col min-h-screen bg-neutral-950 text-white scrollbar-thin overflow-scroll">
|
||||
<Slot />
|
||||
</div>
|
||||
);
|
||||
useOnDocument("DOMContentLoaded", $(() => {
|
||||
AOS.init({
|
||||
once: true,
|
||||
duration: 1000,
|
||||
offset: 100,
|
||||
easing: 'ease-in-out',
|
||||
});
|
||||
}))
|
||||
|
||||
return (
|
||||
<div
|
||||
class="
|
||||
flex flex-col
|
||||
min-h-screen w-full overflow-x-clip
|
||||
bg-neutral-950 text-white
|
||||
">
|
||||
<Slot />
|
||||
</div>
|
||||
);
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue