feat: add download page
This commit is contained in:
parent
efee85cf31
commit
ee21b17935
@ -11,14 +11,18 @@
|
|||||||
--color-yellow-500: oklch(79.5% 0.184 86.047);
|
--color-yellow-500: oklch(79.5% 0.184 86.047);
|
||||||
--color-green-500: oklch(72.3% 0.219 149.579);
|
--color-green-500: oklch(72.3% 0.219 149.579);
|
||||||
--color-blue-500: oklch(62.3% 0.214 259.815);
|
--color-blue-500: oklch(62.3% 0.214 259.815);
|
||||||
|
--color-slate-950: oklch(12.9% 0.042 264.695);
|
||||||
--color-gray-300: oklch(87.2% 0.01 258.338);
|
--color-gray-300: oklch(87.2% 0.01 258.338);
|
||||||
--color-gray-400: oklch(70.7% 0.022 261.325);
|
--color-gray-400: oklch(70.7% 0.022 261.325);
|
||||||
--color-gray-500: oklch(55.1% 0.027 264.364);
|
--color-gray-500: oklch(55.1% 0.027 264.364);
|
||||||
|
--color-gray-800: oklch(27.8% 0.033 256.848);
|
||||||
|
--color-gray-950: oklch(13% 0.028 261.692);
|
||||||
|
--color-white: #fff;
|
||||||
--spacing: 0.25rem;
|
--spacing: 0.25rem;
|
||||||
|
--text-xs: 0.75rem;
|
||||||
|
--text-xs--line-height: calc(1 / 0.75);
|
||||||
--text-sm: 0.875rem;
|
--text-sm: 0.875rem;
|
||||||
--text-sm--line-height: calc(1.25 / 0.875);
|
--text-sm--line-height: calc(1.25 / 0.875);
|
||||||
--text-xl: 1.25rem;
|
|
||||||
--text-xl--line-height: calc(1.75 / 1.25);
|
|
||||||
--text-2xl: 1.5rem;
|
--text-2xl: 1.5rem;
|
||||||
--text-2xl--line-height: calc(2 / 1.5);
|
--text-2xl--line-height: calc(2 / 1.5);
|
||||||
--text-3xl: 1.875rem;
|
--text-3xl: 1.875rem;
|
||||||
@ -31,7 +35,6 @@
|
|||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
|
||||||
--default-transition-duration: 150ms;
|
--default-transition-duration: 150ms;
|
||||||
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
--default-font-family: var(--font-sans);
|
--default-font-family: var(--font-sans);
|
||||||
@ -228,6 +231,9 @@
|
|||||||
.pointer-events-auto {
|
.pointer-events-auto {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
.pointer-events-none {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.collapse {
|
.collapse {
|
||||||
visibility: collapse;
|
visibility: collapse;
|
||||||
}
|
}
|
||||||
@ -240,27 +246,15 @@
|
|||||||
.relative {
|
.relative {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.inset-0 {
|
|
||||||
inset: calc(var(--spacing) * 0);
|
|
||||||
}
|
|
||||||
.top-0 {
|
.top-0 {
|
||||||
top: calc(var(--spacing) * 0);
|
top: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
.right-0 {
|
.right-0 {
|
||||||
right: calc(var(--spacing) * 0);
|
right: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
.bottom-0 {
|
|
||||||
bottom: calc(var(--spacing) * 0);
|
|
||||||
}
|
|
||||||
.left-0 {
|
.left-0 {
|
||||||
left: calc(var(--spacing) * 0);
|
left: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
.left-1 {
|
|
||||||
left: calc(var(--spacing) * 1);
|
|
||||||
}
|
|
||||||
.left-1\/2 {
|
|
||||||
left: calc(1/2 * 100%);
|
|
||||||
}
|
|
||||||
.z-50 {
|
.z-50 {
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
@ -294,6 +288,9 @@
|
|||||||
.mt-1 {
|
.mt-1 {
|
||||||
margin-top: calc(var(--spacing) * 1);
|
margin-top: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
|
.mt-4 {
|
||||||
|
margin-top: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
.mr-3 {
|
.mr-3 {
|
||||||
margin-right: calc(var(--spacing) * 3);
|
margin-right: calc(var(--spacing) * 3);
|
||||||
}
|
}
|
||||||
@ -318,6 +315,10 @@
|
|||||||
.table {
|
.table {
|
||||||
display: table;
|
display: table;
|
||||||
}
|
}
|
||||||
|
.size-4 {
|
||||||
|
width: calc(var(--spacing) * 4);
|
||||||
|
height: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
.size-9 {
|
.size-9 {
|
||||||
width: calc(var(--spacing) * 9);
|
width: calc(var(--spacing) * 9);
|
||||||
height: calc(var(--spacing) * 9);
|
height: calc(var(--spacing) * 9);
|
||||||
@ -343,15 +344,27 @@
|
|||||||
.h-screen {
|
.h-screen {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
.w-2 {
|
||||||
|
width: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.w-3 {
|
||||||
|
width: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
.w-4 {
|
.w-4 {
|
||||||
width: calc(var(--spacing) * 4);
|
width: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
.w-7 {
|
.w-9 {
|
||||||
width: calc(var(--spacing) * 7);
|
width: calc(var(--spacing) * 9);
|
||||||
|
}
|
||||||
|
.w-40 {
|
||||||
|
width: calc(var(--spacing) * 40);
|
||||||
}
|
}
|
||||||
.w-\[39ch\] {
|
.w-\[39ch\] {
|
||||||
width: 39ch;
|
width: 39ch;
|
||||||
}
|
}
|
||||||
|
.w-fit {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -379,28 +392,15 @@
|
|||||||
.grow {
|
.grow {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
.caption-bottom {
|
||||||
|
caption-side: bottom;
|
||||||
|
}
|
||||||
.border-collapse {
|
.border-collapse {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
.origin-left {
|
.origin-left {
|
||||||
transform-origin: left;
|
transform-origin: left;
|
||||||
}
|
}
|
||||||
.-translate-x-1 {
|
|
||||||
--tw-translate-x: calc(var(--spacing) * -1);
|
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
||||||
}
|
|
||||||
.-translate-x-1\/2 {
|
|
||||||
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
||||||
}
|
|
||||||
.-translate-y-4 {
|
|
||||||
--tw-translate-y: calc(var(--spacing) * -4);
|
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
||||||
}
|
|
||||||
.translate-y-4 {
|
|
||||||
--tw-translate-y: calc(var(--spacing) * 4);
|
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
||||||
}
|
|
||||||
.scale-3d {
|
.scale-3d {
|
||||||
scale: var(--tw-scale-x) var(--tw-scale-y) var(--tw-scale-z);
|
scale: var(--tw-scale-x) var(--tw-scale-y) var(--tw-scale-z);
|
||||||
}
|
}
|
||||||
@ -437,6 +437,9 @@
|
|||||||
.items-start {
|
.items-start {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
.justify-between {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
.justify-center {
|
.justify-center {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
@ -452,17 +455,20 @@
|
|||||||
.gap-3 {
|
.gap-3 {
|
||||||
gap: calc(var(--spacing) * 3);
|
gap: calc(var(--spacing) * 3);
|
||||||
}
|
}
|
||||||
.gap-6 {
|
|
||||||
gap: calc(var(--spacing) * 6);
|
|
||||||
}
|
|
||||||
.truncate {
|
.truncate {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.overflow-auto {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
.overflow-hidden {
|
.overflow-hidden {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.rounded-\[4px\] {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
.rounded-full {
|
.rounded-full {
|
||||||
border-radius: calc(infinity * 1px);
|
border-radius: calc(infinity * 1px);
|
||||||
}
|
}
|
||||||
@ -483,6 +489,14 @@
|
|||||||
border-style: var(--tw-border-style);
|
border-style: var(--tw-border-style);
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
}
|
}
|
||||||
|
.border-t {
|
||||||
|
border-top-style: var(--tw-border-style);
|
||||||
|
border-top-width: 1px;
|
||||||
|
}
|
||||||
|
.border-b {
|
||||||
|
border-bottom-style: var(--tw-border-style);
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
.border-dashed {
|
.border-dashed {
|
||||||
--tw-border-style: dashed;
|
--tw-border-style: dashed;
|
||||||
border-style: dashed;
|
border-style: dashed;
|
||||||
@ -490,27 +504,42 @@
|
|||||||
.border-gray-400 {
|
.border-gray-400 {
|
||||||
border-color: var(--color-gray-400);
|
border-color: var(--color-gray-400);
|
||||||
}
|
}
|
||||||
|
.border-input {
|
||||||
|
border-color: var(--input);
|
||||||
|
}
|
||||||
.border-primary {
|
.border-primary {
|
||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
.border-transparent {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
.bg-background {
|
.bg-background {
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
}
|
}
|
||||||
.bg-blue-500 {
|
|
||||||
background-color: var(--color-blue-500);
|
|
||||||
}
|
|
||||||
.bg-destructive {
|
.bg-destructive {
|
||||||
background-color: var(--destructive);
|
background-color: var(--destructive);
|
||||||
}
|
}
|
||||||
.bg-gray-300 {
|
.bg-gray-300 {
|
||||||
background-color: var(--color-gray-300);
|
background-color: var(--color-gray-300);
|
||||||
}
|
}
|
||||||
.bg-gray-500 {
|
.bg-gray-800 {
|
||||||
background-color: var(--color-gray-500);
|
background-color: var(--color-gray-800);
|
||||||
|
}
|
||||||
|
.bg-gray-950 {
|
||||||
|
background-color: var(--color-gray-950);
|
||||||
}
|
}
|
||||||
.bg-green-500 {
|
.bg-green-500 {
|
||||||
background-color: var(--color-green-500);
|
background-color: var(--color-green-500);
|
||||||
}
|
}
|
||||||
|
.bg-muted {
|
||||||
|
background-color: var(--muted);
|
||||||
|
}
|
||||||
|
.bg-muted\/50 {
|
||||||
|
background-color: var(--muted);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--muted) 50%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
.bg-popover {
|
.bg-popover {
|
||||||
background-color: var(--popover);
|
background-color: var(--popover);
|
||||||
}
|
}
|
||||||
@ -526,8 +555,8 @@
|
|||||||
.bg-selection {
|
.bg-selection {
|
||||||
background-color: var(--selection);
|
background-color: var(--selection);
|
||||||
}
|
}
|
||||||
.bg-yellow-500 {
|
.bg-white {
|
||||||
background-color: var(--color-yellow-500);
|
background-color: var(--color-white);
|
||||||
}
|
}
|
||||||
.p-0 {
|
.p-0 {
|
||||||
padding: calc(var(--spacing) * 0);
|
padding: calc(var(--spacing) * 0);
|
||||||
@ -541,12 +570,21 @@
|
|||||||
.p-4 {
|
.p-4 {
|
||||||
padding: calc(var(--spacing) * 4);
|
padding: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
.p-5 {
|
||||||
|
padding: calc(var(--spacing) * 5);
|
||||||
|
}
|
||||||
.p-7 {
|
.p-7 {
|
||||||
padding: calc(var(--spacing) * 7);
|
padding: calc(var(--spacing) * 7);
|
||||||
}
|
}
|
||||||
|
.p-8 {
|
||||||
|
padding: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
.p-10 {
|
.p-10 {
|
||||||
padding: calc(var(--spacing) * 10);
|
padding: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
|
.px-2 {
|
||||||
|
padding-inline: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
.px-3 {
|
.px-3 {
|
||||||
padding-inline: calc(var(--spacing) * 3);
|
padding-inline: calc(var(--spacing) * 3);
|
||||||
}
|
}
|
||||||
@ -556,6 +594,12 @@
|
|||||||
.px-6 {
|
.px-6 {
|
||||||
padding-inline: calc(var(--spacing) * 6);
|
padding-inline: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
|
.py-0 {
|
||||||
|
padding-block: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
|
.py-0\.5 {
|
||||||
|
padding-block: calc(var(--spacing) * 0.5);
|
||||||
|
}
|
||||||
.py-2 {
|
.py-2 {
|
||||||
padding-block: calc(var(--spacing) * 2);
|
padding-block: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
@ -568,12 +612,15 @@
|
|||||||
.pl-2 {
|
.pl-2 {
|
||||||
padding-left: calc(var(--spacing) * 2);
|
padding-left: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
.pl-4 {
|
|
||||||
padding-left: calc(var(--spacing) * 4);
|
|
||||||
}
|
|
||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.text-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.align-middle {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
.text-2xl {
|
.text-2xl {
|
||||||
font-size: var(--text-2xl);
|
font-size: var(--text-2xl);
|
||||||
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
||||||
@ -590,6 +637,10 @@
|
|||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
line-height: var(--tw-leading, var(--text-sm--line-height));
|
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||||
}
|
}
|
||||||
|
.text-xs {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
line-height: var(--tw-leading, var(--text-xs--line-height));
|
||||||
|
}
|
||||||
.leading-none {
|
.leading-none {
|
||||||
--tw-leading: 1;
|
--tw-leading: 1;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -611,9 +662,15 @@
|
|||||||
.text-destructive {
|
.text-destructive {
|
||||||
color: var(--destructive);
|
color: var(--destructive);
|
||||||
}
|
}
|
||||||
|
.text-foreground {
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
.text-green-500 {
|
.text-green-500 {
|
||||||
color: var(--color-green-500);
|
color: var(--color-green-500);
|
||||||
}
|
}
|
||||||
|
.text-muted-foreground {
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
}
|
||||||
.text-popover-foreground {
|
.text-popover-foreground {
|
||||||
color: var(--popover-foreground);
|
color: var(--popover-foreground);
|
||||||
}
|
}
|
||||||
@ -629,6 +686,9 @@
|
|||||||
.text-secondary-foreground {
|
.text-secondary-foreground {
|
||||||
color: var(--secondary-foreground);
|
color: var(--secondary-foreground);
|
||||||
}
|
}
|
||||||
|
.text-white {
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
.text-yellow-500 {
|
.text-yellow-500 {
|
||||||
color: var(--color-yellow-500);
|
color: var(--color-yellow-500);
|
||||||
}
|
}
|
||||||
@ -670,11 +730,26 @@
|
|||||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||||
}
|
}
|
||||||
|
.transition-\[color\,box-shadow\] {
|
||||||
|
transition-property: color,box-shadow;
|
||||||
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
|
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||||
|
}
|
||||||
.transition-all {
|
.transition-all {
|
||||||
transition-property: all;
|
transition-property: all;
|
||||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||||
}
|
}
|
||||||
|
.transition-colors {
|
||||||
|
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
|
||||||
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
|
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||||
|
}
|
||||||
|
.transition-shadow {
|
||||||
|
transition-property: box-shadow;
|
||||||
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
|
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||||
|
}
|
||||||
.transition-transform {
|
.transition-transform {
|
||||||
transition-property: transform, translate, scale, rotate;
|
transition-property: transform, translate, scale, rotate;
|
||||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
@ -688,14 +763,15 @@
|
|||||||
--tw-ease: linear;
|
--tw-ease: linear;
|
||||||
transition-timing-function: linear;
|
transition-timing-function: linear;
|
||||||
}
|
}
|
||||||
.ease-out {
|
|
||||||
--tw-ease: var(--ease-out);
|
|
||||||
transition-timing-function: var(--ease-out);
|
|
||||||
}
|
|
||||||
.outline-none {
|
.outline-none {
|
||||||
--tw-outline-style: none;
|
--tw-outline-style: none;
|
||||||
outline-style: none;
|
outline-style: none;
|
||||||
}
|
}
|
||||||
|
.peer-checked\:opacity-100 {
|
||||||
|
&:is(:where(.peer):checked ~ *) {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
.before\:absolute {
|
.before\:absolute {
|
||||||
&::before {
|
&::before {
|
||||||
content: var(--tw-content);
|
content: var(--tw-content);
|
||||||
@ -762,6 +838,11 @@
|
|||||||
background-color: var(--primary);
|
background-color: var(--primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.checked\:text-primary-foreground {
|
||||||
|
&:checked {
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
.checked\:before\:visible {
|
.checked\:before\:visible {
|
||||||
&:checked {
|
&:checked {
|
||||||
&::before {
|
&::before {
|
||||||
@ -794,6 +875,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.hover\:bg-muted\/50 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--muted);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--muted) 50%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.hover\:bg-primary\/90 {
|
.hover\:bg-primary\/90 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
@ -894,6 +985,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.focus-visible\:outline-none {
|
||||||
|
&:focus-visible {
|
||||||
|
--tw-outline-style: none;
|
||||||
|
outline-style: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
.disabled\:pointer-events-none {
|
.disabled\:pointer-events-none {
|
||||||
&:disabled {
|
&:disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@ -1009,6 +1106,11 @@
|
|||||||
right: calc(var(--spacing) * 0);
|
right: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.data-\[tui-table-state-selected\]\:bg-muted {
|
||||||
|
&[data-tui-table-state-selected] {
|
||||||
|
background-color: var(--muted);
|
||||||
|
}
|
||||||
|
}
|
||||||
.data-\[variant\=default\]\:bg-gray-500 {
|
.data-\[variant\=default\]\:bg-gray-500 {
|
||||||
&[data-variant="default"] {
|
&[data-variant="default"] {
|
||||||
background-color: var(--color-gray-500);
|
background-color: var(--color-gray-500);
|
||||||
@ -1034,6 +1136,16 @@
|
|||||||
background-color: var(--color-yellow-500);
|
background-color: var(--color-yellow-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sm\:flex {
|
||||||
|
@media (width >= 40rem) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.md\:w-3\/4 {
|
||||||
|
@media (width >= 48rem) {
|
||||||
|
width: calc(3/4 * 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
.md\:max-w-\[420px\] {
|
.md\:max-w-\[420px\] {
|
||||||
@media (width >= 48rem) {
|
@media (width >= 48rem) {
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
@ -1060,6 +1172,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dark\:text-white {
|
||||||
|
&:where(.dark, .dark *) {
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
.dark\:hover\:bg-accent\/50 {
|
.dark\:hover\:bg-accent\/50 {
|
||||||
&:where(.dark, .dark *) {
|
&:where(.dark, .dark *) {
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -1120,6 +1237,102 @@
|
|||||||
height: calc(var(--spacing) * 4);
|
height: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.\[\&_tr\]\:border-b {
|
||||||
|
& tr {
|
||||||
|
border-bottom-style: var(--tw-border-style);
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[\&_tr\:last-child\]\:border-0 {
|
||||||
|
& tr:last-child {
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
border-width: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0 {
|
||||||
|
&:has([role=checkbox]) {
|
||||||
|
padding-right: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[\&\>\[role\=checkbox\]\]\:translate-y-\[2px\] {
|
||||||
|
&>[role=checkbox] {
|
||||||
|
--tw-translate-y: 2px;
|
||||||
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[\&\>svg\]\:pointer-events-none {
|
||||||
|
&>svg {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[\&\>svg\]\:size-3 {
|
||||||
|
&>svg {
|
||||||
|
width: calc(var(--spacing) * 3);
|
||||||
|
height: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[\&\>tr\]\:last\:border-b-0 {
|
||||||
|
&>tr {
|
||||||
|
&:last-child {
|
||||||
|
border-bottom-style: var(--tw-border-style);
|
||||||
|
border-bottom-width: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[a\&\]\:hover\:bg-accent {
|
||||||
|
a& {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[a\&\]\:hover\:bg-destructive\/90 {
|
||||||
|
a& {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--destructive);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--destructive) 90%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[a\&\]\:hover\:bg-primary\/90 {
|
||||||
|
a& {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--primary);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--primary) 90%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[a\&\]\:hover\:bg-secondary\/90 {
|
||||||
|
a& {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--secondary);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--secondary) 90%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[a\&\]\:hover\:text-accent-foreground {
|
||||||
|
a& {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
color: var(--accent-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.65rem;
|
--radius: 0.65rem;
|
||||||
@ -1207,21 +1420,6 @@
|
|||||||
font-feature-settings: "rlig" 1, "calt" 1;
|
font-feature-settings: "rlig" 1, "calt" 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@property --tw-translate-x {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 0;
|
|
||||||
}
|
|
||||||
@property --tw-translate-y {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 0;
|
|
||||||
}
|
|
||||||
@property --tw-translate-z {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 0;
|
|
||||||
}
|
|
||||||
@property --tw-scale-x {
|
@property --tw-scale-x {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
@ -1353,12 +1551,24 @@
|
|||||||
initial-value: "";
|
initial-value: "";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
}
|
}
|
||||||
|
@property --tw-translate-x {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
@property --tw-translate-y {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
@property --tw-translate-z {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
@layer properties {
|
@layer properties {
|
||||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||||
*, ::before, ::after, ::backdrop {
|
*, ::before, ::after, ::backdrop {
|
||||||
--tw-translate-x: 0;
|
|
||||||
--tw-translate-y: 0;
|
|
||||||
--tw-translate-z: 0;
|
|
||||||
--tw-scale-x: 1;
|
--tw-scale-x: 1;
|
||||||
--tw-scale-y: 1;
|
--tw-scale-y: 1;
|
||||||
--tw-scale-z: 1;
|
--tw-scale-z: 1;
|
||||||
@ -1388,6 +1598,9 @@
|
|||||||
--tw-duration: initial;
|
--tw-duration: initial;
|
||||||
--tw-ease: initial;
|
--tw-ease: initial;
|
||||||
--tw-content: "";
|
--tw-content: "";
|
||||||
|
--tw-translate-x: 0;
|
||||||
|
--tw-translate-y: 0;
|
||||||
|
--tw-translate-z: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
58
components/badge/badge.templ
Normal file
58
components/badge/badge.templ
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// templui component badge - version: v0.94.0 installed by templui v0.94.0
|
||||||
|
package badge
|
||||||
|
|
||||||
|
import "git.hafen.run/lukas/timeshare/utils"
|
||||||
|
|
||||||
|
type Variant string
|
||||||
|
|
||||||
|
const (
|
||||||
|
VariantDefault Variant = "default"
|
||||||
|
VariantSecondary Variant = "secondary"
|
||||||
|
VariantDestructive Variant = "destructive"
|
||||||
|
VariantOutline Variant = "outline"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Props struct {
|
||||||
|
ID string
|
||||||
|
Class string
|
||||||
|
Attributes templ.Attributes
|
||||||
|
Variant Variant
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Badge(props ...Props) {
|
||||||
|
{{ var p Props }}
|
||||||
|
if len(props) > 0 {
|
||||||
|
{{ p = props[0] }}
|
||||||
|
}
|
||||||
|
<span
|
||||||
|
if p.ID != "" {
|
||||||
|
id={ p.ID }
|
||||||
|
}
|
||||||
|
class={
|
||||||
|
utils.TwMerge(
|
||||||
|
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
"transition-[color,box-shadow] overflow-hidden",
|
||||||
|
p.variantClasses(),
|
||||||
|
p.Class,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
{ p.Attributes... }
|
||||||
|
>
|
||||||
|
{ children... }
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Props) variantClasses() string {
|
||||||
|
switch p.Variant {
|
||||||
|
case VariantDestructive:
|
||||||
|
return "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60"
|
||||||
|
case VariantOutline:
|
||||||
|
return "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground"
|
||||||
|
case VariantSecondary:
|
||||||
|
return "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90"
|
||||||
|
default:
|
||||||
|
return "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90"
|
||||||
|
}
|
||||||
|
}
|
70
components/checkbox/checkbox.templ
Normal file
70
components/checkbox/checkbox.templ
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// templui component checkbox - version: v0.94.0 installed by templui v0.94.0
|
||||||
|
package checkbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.hafen.run/lukas/timeshare/components/icon"
|
||||||
|
"git.hafen.run/lukas/timeshare/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Props struct {
|
||||||
|
ID string
|
||||||
|
Class string
|
||||||
|
Attributes templ.Attributes
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
Disabled bool
|
||||||
|
Required bool
|
||||||
|
Checked bool
|
||||||
|
Form string
|
||||||
|
Icon templ.Component
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Checkbox(props ...Props) {
|
||||||
|
{{ var p Props }}
|
||||||
|
if len(props) > 0 {
|
||||||
|
{{ p = props[0] }}
|
||||||
|
}
|
||||||
|
<div class="relative inline-flex items-center">
|
||||||
|
<input
|
||||||
|
checked?={ p.Checked }
|
||||||
|
disabled?={ p.Disabled }
|
||||||
|
required?={ p.Required }
|
||||||
|
if p.ID != "" {
|
||||||
|
id={ p.ID }
|
||||||
|
}
|
||||||
|
if p.Name != "" {
|
||||||
|
name={ p.Name }
|
||||||
|
}
|
||||||
|
if p.Value != "" {
|
||||||
|
value={ p.Value }
|
||||||
|
} else {
|
||||||
|
value="on"
|
||||||
|
}
|
||||||
|
if p.Form != "" {
|
||||||
|
form={ p.Form }
|
||||||
|
}
|
||||||
|
type="checkbox"
|
||||||
|
class={
|
||||||
|
utils.TwMerge(
|
||||||
|
"peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs",
|
||||||
|
"focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:border-ring",
|
||||||
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
"checked:bg-primary checked:text-primary-foreground checked:border-primary",
|
||||||
|
"appearance-none cursor-pointer transition-shadow",
|
||||||
|
"relative",
|
||||||
|
p.Class,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
{ p.Attributes... }
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute left-0 top-0 h-4 w-4 pointer-events-none flex items-center justify-center text-primary-foreground opacity-0 peer-checked:opacity-100"
|
||||||
|
>
|
||||||
|
if p.Icon != nil {
|
||||||
|
@p.Icon
|
||||||
|
} else {
|
||||||
|
@icon.Check(icon.Props{Size: 14})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
204
components/table/table.templ
Normal file
204
components/table/table.templ
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
// templui component table - version: v0.94.0 installed by templui v0.94.0
|
||||||
|
package table
|
||||||
|
|
||||||
|
import "git.hafen.run/lukas/timeshare/utils"
|
||||||
|
|
||||||
|
type Props struct {
|
||||||
|
ID string
|
||||||
|
Class string
|
||||||
|
Attributes templ.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeaderProps struct {
|
||||||
|
ID string
|
||||||
|
Class string
|
||||||
|
Attributes templ.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
type BodyProps struct {
|
||||||
|
ID string
|
||||||
|
Class string
|
||||||
|
Attributes templ.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
type FooterProps struct {
|
||||||
|
ID string
|
||||||
|
Class string
|
||||||
|
Attributes templ.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
type RowProps struct {
|
||||||
|
ID string
|
||||||
|
Class string
|
||||||
|
Attributes templ.Attributes
|
||||||
|
Selected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeadProps struct {
|
||||||
|
ID string
|
||||||
|
Class string
|
||||||
|
Attributes templ.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
type CellProps struct {
|
||||||
|
ID string
|
||||||
|
Class string
|
||||||
|
Attributes templ.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaptionProps struct {
|
||||||
|
ID string
|
||||||
|
Class string
|
||||||
|
Attributes templ.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Table(props ...Props) {
|
||||||
|
{{ var p Props }}
|
||||||
|
if len(props) > 0 {
|
||||||
|
{{ p = props[0] }}
|
||||||
|
}
|
||||||
|
<div class="relative w-full overflow-auto">
|
||||||
|
<table
|
||||||
|
if p.ID != "" {
|
||||||
|
id={ p.ID }
|
||||||
|
}
|
||||||
|
class={ utils.TwMerge("w-full caption-bottom text-sm", p.Class) }
|
||||||
|
{ p.Attributes... }
|
||||||
|
>
|
||||||
|
{ children... }
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Header(props ...HeaderProps) {
|
||||||
|
{{ var p HeaderProps }}
|
||||||
|
if len(props) > 0 {
|
||||||
|
{{ p = props[0] }}
|
||||||
|
}
|
||||||
|
<thead
|
||||||
|
if p.ID != "" {
|
||||||
|
id={ p.ID }
|
||||||
|
}
|
||||||
|
class={ utils.TwMerge("[&_tr]:border-b", p.Class) }
|
||||||
|
{ p.Attributes... }
|
||||||
|
>
|
||||||
|
{ children... }
|
||||||
|
</thead>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Body(props ...BodyProps) {
|
||||||
|
{{ var p BodyProps }}
|
||||||
|
if len(props) > 0 {
|
||||||
|
{{ p = props[0] }}
|
||||||
|
}
|
||||||
|
<tbody
|
||||||
|
if p.ID != "" {
|
||||||
|
id={ p.ID }
|
||||||
|
}
|
||||||
|
class={ utils.TwMerge("[&_tr:last-child]:border-0", p.Class) }
|
||||||
|
{ p.Attributes... }
|
||||||
|
>
|
||||||
|
{ children... }
|
||||||
|
</tbody>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Footer(props ...FooterProps) {
|
||||||
|
{{ var p FooterProps }}
|
||||||
|
if len(props) > 0 {
|
||||||
|
{{ p = props[0] }}
|
||||||
|
}
|
||||||
|
<tfoot
|
||||||
|
if p.ID != "" {
|
||||||
|
id={ p.ID }
|
||||||
|
}
|
||||||
|
class={ utils.TwMerge("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", p.Class) }
|
||||||
|
{ p.Attributes... }
|
||||||
|
>
|
||||||
|
{ children... }
|
||||||
|
</tfoot>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Row(props ...RowProps) {
|
||||||
|
{{ var p RowProps }}
|
||||||
|
if len(props) > 0 {
|
||||||
|
{{ p = props[0] }}
|
||||||
|
}
|
||||||
|
<tr
|
||||||
|
if p.ID != "" {
|
||||||
|
id={ p.ID }
|
||||||
|
}
|
||||||
|
class={
|
||||||
|
utils.TwMerge(
|
||||||
|
"border-b transition-colors hover:bg-muted/50",
|
||||||
|
utils.If(p.Selected, "data-[tui-table-state-selected]:bg-muted"),
|
||||||
|
p.Class,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if p.Selected {
|
||||||
|
data-tui-table-state-selected
|
||||||
|
}
|
||||||
|
{ p.Attributes... }
|
||||||
|
>
|
||||||
|
{ children... }
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Head(props ...HeadProps) {
|
||||||
|
{{ var p HeadProps }}
|
||||||
|
if len(props) > 0 {
|
||||||
|
{{ p = props[0] }}
|
||||||
|
}
|
||||||
|
<th
|
||||||
|
if p.ID != "" {
|
||||||
|
id={ p.ID }
|
||||||
|
}
|
||||||
|
class={
|
||||||
|
utils.TwMerge(
|
||||||
|
"h-10 px-2 text-left align-middle font-medium text-muted-foreground",
|
||||||
|
"[&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
p.Class,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
{ p.Attributes... }
|
||||||
|
>
|
||||||
|
{ children... }
|
||||||
|
</th>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Cell(props ...CellProps) {
|
||||||
|
{{ var p CellProps }}
|
||||||
|
if len(props) > 0 {
|
||||||
|
{{ p = props[0] }}
|
||||||
|
}
|
||||||
|
<td
|
||||||
|
if p.ID != "" {
|
||||||
|
id={ p.ID }
|
||||||
|
}
|
||||||
|
class={
|
||||||
|
utils.TwMerge(
|
||||||
|
"p-2 align-middle",
|
||||||
|
"[&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
p.Class,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
{ p.Attributes... }
|
||||||
|
>
|
||||||
|
{ children... }
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Caption(props ...CaptionProps) {
|
||||||
|
{{ var p CaptionProps }}
|
||||||
|
if len(props) > 0 {
|
||||||
|
{{ p = props[0] }}
|
||||||
|
}
|
||||||
|
<caption
|
||||||
|
if p.ID != "" {
|
||||||
|
id={ p.ID }
|
||||||
|
}
|
||||||
|
class={ utils.TwMerge("mt-4 text-sm text-muted-foreground", p.Class) }
|
||||||
|
{ p.Attributes... }
|
||||||
|
>
|
||||||
|
{ children... }
|
||||||
|
</caption>
|
||||||
|
}
|
138
handlers/download.go
Normal file
138
handlers/download.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gabriel-vasile/mimetype"
|
||||||
|
"github.com/valkey-io/valkey-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DownloadFile(client valkey.Client) func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
upload_id := r.PathValue("upload_id")
|
||||||
|
if upload_id == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file_id := r.PathValue("file_id")
|
||||||
|
if file_id == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files_json, err := client.Do(r.Context(),
|
||||||
|
client.B().Get().Key(upload_id).Build()).ToString()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var files []string
|
||||||
|
err = json.Unmarshal([]byte(files_json), &files)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
found_id := 0
|
||||||
|
for i, id := range files {
|
||||||
|
if id == file_id {
|
||||||
|
found = true
|
||||||
|
found_id = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
http.Error(w, "not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
file := files[found_id]
|
||||||
|
|
||||||
|
filename, err := client.Do(r.Context(), client.B().Get().Key(file+":filename").Build()).ToString()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileContents, err := client.Do(r.Context(), client.B().Get().Key(file+":contents").Build()).AsBytes()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Add("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
||||||
|
mime := mimetype.Detect(fileContents)
|
||||||
|
w.Header().Add("mimetype", mime.String())
|
||||||
|
w.Write(fileContents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func DownloadFilesZipped(client valkey.Client) func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
upload_id := r.PathValue("upload_id")
|
||||||
|
if upload_id == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files_json, err := client.Do(r.Context(),
|
||||||
|
client.B().Get().Key(upload_id).Build()).ToString()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var files []string
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(files_json), &files)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(files) == 1 {
|
||||||
|
file := files[0]
|
||||||
|
|
||||||
|
filename, err := client.Do(r.Context(), client.B().Get().Key(file+":filename").Build()).ToString()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileContents, err := client.Do(r.Context(), client.B().Get().Key(file+":contents").Build()).AsBytes()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Add("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
||||||
|
mime := mimetype.Detect(fileContents)
|
||||||
|
w.Header().Add("mimetype", mime.String())
|
||||||
|
w.Write(fileContents)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zipBuf := new(bytes.Buffer)
|
||||||
|
zipWriter := zip.NewWriter(zipBuf)
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
filename, err := client.Do(r.Context(), client.B().Get().Key(file+":filename").Build()).ToString()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fileContents, err := client.Do(r.Context(), client.B().Get().Key(file+":contents").Build()).AsBytes()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
w, err := zipWriter.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
w.Write(fileContents)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
zipWriter.Close()
|
||||||
|
w.Header().Add("mimetype", "application/zip")
|
||||||
|
w.Header().Add("Content-Disposition", `attachment; filename="timeshare-download.zip"`)
|
||||||
|
io.Copy(w, zipBuf)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
58
handlers/list.go
Normal file
58
handlers/list.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.hafen.run/lukas/timeshare/pages"
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
"github.com/valkey-io/valkey-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListFiles(client valkey.Client) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
upload_id := r.PathValue("upload_id")
|
||||||
|
if upload_id == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expiresInt, err := client.Do(r.Context(), client.B().Ttl().Key(upload_id).Build()).AsInt64()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expiresIn := time.Second * time.Duration(expiresInt)
|
||||||
|
expires := time.Now().Add(expiresIn)
|
||||||
|
|
||||||
|
files_json, err := client.Do(r.Context(),
|
||||||
|
client.B().Get().Key(upload_id).Build()).ToString()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileIds []string
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(files_json), &fileIds)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files := make([]pages.File, len(fileIds))
|
||||||
|
for i, fileid := range fileIds {
|
||||||
|
filename, err := client.Do(r.Context(), client.B().Get().Key(fileid+":filename").Build()).ToString()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
files[i] = pages.File{
|
||||||
|
Filename: filename,
|
||||||
|
Key: fileid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ.Handler(pages.ListShareContents(expires, upload_id, files)).ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
69
handlers/upload.go
Normal file
69
handlers/upload.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.hafen.run/lukas/timeshare/pages"
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/valkey-io/valkey-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UploadFiles(client valkey.Client) func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := r.ParseMultipartForm(125_000_000) // 1G max memory usage
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form := r.MultipartForm
|
||||||
|
expiry_values := form.Value["expiry"]
|
||||||
|
expiry, err := time.ParseDuration(expiry_values[0])
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form_files := form.File["files"]
|
||||||
|
files := make([]string, len(form_files))
|
||||||
|
for i := range form_files {
|
||||||
|
fid := uuid.New().String()
|
||||||
|
files[i] = fid
|
||||||
|
f, err := form_files[i].Open()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
_, err = io.Copy(buf, f)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = client.Do(r.Context(), client.B().Set().Key(fid+":filename").Value(form_files[i].Filename).Ex(expiry).Build()).Error()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.Do(r.Context(), client.B().Set().Key(fid+":contents").Value(valkey.BinaryString(buf.Bytes())).Ex(expiry).Build()).Error()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := uuid.New()
|
||||||
|
files_b, _ := json.Marshal(files)
|
||||||
|
err = client.Do(r.Context(), client.B().Set().Key(uid.String()).Value(string(files_b)).Ex(expiry).Build()).Error()
|
||||||
|
|
||||||
|
templ.Handler(pages.Upload([]pages.Expiry{
|
||||||
|
{DurationCode: "24h", DurationName: "24 Hours"},
|
||||||
|
{DurationCode: "36h", DurationName: "36 Hours"},
|
||||||
|
{DurationCode: "48h", DurationName: "48 Hours"},
|
||||||
|
{DurationCode: "168h", DurationName: "1 Week"},
|
||||||
|
}, fmt.Sprintf("https://share.lukaswerner.com/%s", uid.String()))).ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
132
main.go
132
main.go
@ -1,22 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
|
||||||
"bytes"
|
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
|
"git.hafen.run/lukas/timeshare/handlers"
|
||||||
"git.hafen.run/lukas/timeshare/pages"
|
"git.hafen.run/lukas/timeshare/pages"
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
"github.com/gabriel-vasile/mimetype"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/valkey-io/valkey-go"
|
"github.com/valkey-io/valkey-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -46,125 +39,10 @@ func main() {
|
|||||||
{DurationCode: "168h", DurationName: "1 Week"},
|
{DurationCode: "168h", DurationName: "1 Week"},
|
||||||
}, "")))
|
}, "")))
|
||||||
|
|
||||||
http.Handle("POST /upload", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
http.Handle("POST /upload", http.HandlerFunc(handlers.UploadFiles(client)))
|
||||||
err := r.ParseMultipartForm(125_000_000) // 1G max memory usage
|
http.Handle("/download/{upload_id}/zip", http.HandlerFunc(handlers.DownloadFilesZipped(client)))
|
||||||
if err != nil {
|
http.Handle("/download/{upload_id}/{file_id}", http.HandlerFunc(handlers.DownloadFile(client)))
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Handle("/{upload_id}", http.HandlerFunc(handlers.ListFiles(client)))
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
form := r.MultipartForm
|
|
||||||
expiry_values := form.Value["expiry"]
|
|
||||||
expiry, err := time.ParseDuration(expiry_values[0])
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
form_files := form.File["files"]
|
|
||||||
files := make([]string, len(form_files))
|
|
||||||
for i := range form_files {
|
|
||||||
fid := uuid.New().String()
|
|
||||||
files[i] = fid
|
|
||||||
f, err := form_files[i].Open()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
_, err = io.Copy(buf, f)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err = client.Do(r.Context(), client.B().Set().Key(fid+":filename").Value(form_files[i].Filename).Ex(expiry).Build()).Error()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = client.Do(r.Context(), client.B().Set().Key(fid+":contents").Value(valkey.BinaryString(buf.Bytes())).Ex(expiry).Build()).Error()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uid := uuid.New()
|
|
||||||
files_b, _ := json.Marshal(files)
|
|
||||||
err = client.Do(r.Context(), client.B().Set().Key(uid.String()).Value(string(files_b)).Ex(expiry).Build()).Error()
|
|
||||||
|
|
||||||
templ.Handler(pages.Upload([]pages.Expiry{
|
|
||||||
{DurationCode: "24h", DurationName: "24 Hours"},
|
|
||||||
{DurationCode: "36h", DurationName: "36 Hours"},
|
|
||||||
{DurationCode: "48h", DurationName: "48 Hours"},
|
|
||||||
{DurationCode: "168h", DurationName: "1 Week"},
|
|
||||||
}, fmt.Sprintf("https://share.lukaswerner.com/%s", uid.String()))).ServeHTTP(w, r)
|
|
||||||
}))
|
|
||||||
|
|
||||||
http.Handle("/{upload_id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
upload_id := r.PathValue("upload_id")
|
|
||||||
if upload_id == "" {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
files_json, err := client.Do(r.Context(),
|
|
||||||
client.B().Get().Key(upload_id).Build()).ToString()
|
|
||||||
if err != nil {
|
|
||||||
|
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var files []string
|
|
||||||
|
|
||||||
err = json.Unmarshal([]byte(files_json), &files)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(files) == 1 {
|
|
||||||
file := files[0]
|
|
||||||
|
|
||||||
filename, err := client.Do(r.Context(), client.B().Get().Key(file+":filename").Build()).ToString()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fileContents, err := client.Do(r.Context(), client.B().Get().Key(file+":contents").Build()).AsBytes()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Add("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
|
||||||
mime := mimetype.Detect(fileContents)
|
|
||||||
w.Header().Add("mimetype", mime.String())
|
|
||||||
w.Write(fileContents)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
zipBuf := new(bytes.Buffer)
|
|
||||||
zipWriter := zip.NewWriter(zipBuf)
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
filename, err := client.Do(r.Context(), client.B().Get().Key(file+":filename").Build()).ToString()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fileContents, err := client.Do(r.Context(), client.B().Get().Key(file+":contents").Build()).AsBytes()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
w, err := zipWriter.Create(filename)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
w.Write(fileContents)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
zipWriter.Close()
|
|
||||||
w.Header().Add("mimetype", "application/zip")
|
|
||||||
w.Header().Add("Content-Disposition", `attachment; filename="timeshare-download.zip"`)
|
|
||||||
io.Copy(w, zipBuf)
|
|
||||||
|
|
||||||
}))
|
|
||||||
http.Handle("/", templ.Handler(pages.Index()))
|
http.Handle("/", templ.Handler(pages.Index()))
|
||||||
log.Println(http.ListenAndServe(":8080", nil))
|
log.Println(http.ListenAndServe(":8080", nil))
|
||||||
}
|
}
|
||||||
|
2
makefile
2
makefile
@ -1,6 +1,6 @@
|
|||||||
# Run templ generation in watch mode
|
# Run templ generation in watch mode
|
||||||
templ:
|
templ:
|
||||||
templ generate --watch --proxy="http://localhost:8090" --open-browser=false
|
templ generate --watch --proxy="http://localhost:8080" --open-browser=true
|
||||||
|
|
||||||
# Run air for Go hot reload
|
# Run air for Go hot reload
|
||||||
server:
|
server:
|
||||||
|
143
pages/share_list.templ
Normal file
143
pages/share_list.templ
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.hafen.run/lukas/timeshare/components/badge"
|
||||||
|
"git.hafen.run/lukas/timeshare/components/button"
|
||||||
|
"git.hafen.run/lukas/timeshare/components/checkbox"
|
||||||
|
"git.hafen.run/lukas/timeshare/components/icon"
|
||||||
|
"git.hafen.run/lukas/timeshare/components/table"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Filename string
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
templ filetypeIcon(filename string) {
|
||||||
|
switch path.Ext(filename) {
|
||||||
|
case ".jpg",".jpeg", ".png",".heic", ".webp", ".avif", ".gif", ".tiff":
|
||||||
|
@icon.FileImage()
|
||||||
|
case ".mp3",".flac", ".m4a",".m4b", ".wav", ".midi":
|
||||||
|
@icon.FileAudio()
|
||||||
|
case ".mov",".mp4":
|
||||||
|
@icon.FileVideo()
|
||||||
|
case ".txt",".md":
|
||||||
|
@icon.FileText()
|
||||||
|
case ".zip":
|
||||||
|
@icon.FileArchive()
|
||||||
|
case ".xlsx", ".csv":
|
||||||
|
@icon.FileSpreadsheet()
|
||||||
|
case ".pdf", ".docx", ".doc":
|
||||||
|
@icon.FileText()
|
||||||
|
case ".epub":
|
||||||
|
@icon.BookMarked()
|
||||||
|
case ".stl", ".3mf", ".step":
|
||||||
|
@icon.FileAxis3d()
|
||||||
|
case ".json":
|
||||||
|
@icon.FileJson()
|
||||||
|
default:
|
||||||
|
@icon.File()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ ListShareContents(expires time.Time, shareid string, files []File) {
|
||||||
|
@PageSkeleton("Download - Time Share") {
|
||||||
|
<div class="flex flex-col p-10 w-full">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="sm:flex gap-2 items-center">
|
||||||
|
<h1 class="text-2xl">Download from Share</h1>
|
||||||
|
@badge.Badge(badge.Props{
|
||||||
|
Class: "flex gap-2 items-center dark:text-white",
|
||||||
|
}) {
|
||||||
|
@icon.Timer()
|
||||||
|
{ expires.Format("03:04PM Jan 2") }
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<a href={ "/download/" + shareid + "/zip" } id="download-link">
|
||||||
|
@button.Button(button.Props{ID: "download-button"}) {
|
||||||
|
@icon.FolderDown()
|
||||||
|
Download All
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col justify-center items-center">
|
||||||
|
<div class="w-full md:w-3/4">
|
||||||
|
@table.Table() {
|
||||||
|
@table.Header() {
|
||||||
|
@table.Row() {
|
||||||
|
@table.Head()
|
||||||
|
@table.Head() {
|
||||||
|
Filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@table.Body() {
|
||||||
|
for _, file := range files {
|
||||||
|
@table.Row() {
|
||||||
|
@table.Cell(table.CellProps{Class: "w-2"}) {
|
||||||
|
@checkbox.Checkbox(checkbox.Props{
|
||||||
|
Name: file.Key,
|
||||||
|
Value: file.Key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@table.Cell() {
|
||||||
|
<span class="flex gap-2 items-center">
|
||||||
|
@filetypeIcon(file.Filename)
|
||||||
|
{ file.Filename }
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
let checkedIds = [];
|
||||||
|
let downloadButton;
|
||||||
|
let downloadLink;
|
||||||
|
let downloadLinkZip;
|
||||||
|
let downloadIcon;
|
||||||
|
let share_id;
|
||||||
|
window.onload = () => {
|
||||||
|
downloadButton = document.querySelector("#download-button");
|
||||||
|
downloadLink = document.querySelector("#download-link");
|
||||||
|
downloadLinkZip = document.querySelector("#download-link").href;
|
||||||
|
downloadIcon = downloadLink.querySelector("svg").outerHTML;
|
||||||
|
share_id = window.location.pathname;
|
||||||
|
};
|
||||||
|
document.querySelectorAll("input[type=checkbox]").forEach(el=>{
|
||||||
|
el.addEventListener("click", el=>{
|
||||||
|
checkedIds = Array.from(document.querySelectorAll("input[type=checkbox]:checked").values().map(el=>el.value))
|
||||||
|
if (checkedIds.length == 0) {
|
||||||
|
// nothing selected. download all
|
||||||
|
downloadButton.innerHTML = downloadIcon + "Download All";
|
||||||
|
downloadLink.href = downloadLinkZip;
|
||||||
|
downloadButton.onclick = () => {};
|
||||||
|
} else {
|
||||||
|
// things selected!
|
||||||
|
downloadButton.innerHTML = downloadIcon + "Download Selected";
|
||||||
|
downloadLink.href = "#";
|
||||||
|
downloadButton.onclick = () => {
|
||||||
|
let delay = 0;
|
||||||
|
for (const id of checkedIds) {
|
||||||
|
console.log(id);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = "/download" + share_id + "/" + id;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
setTimeout(() => {
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
}, delay);
|
||||||
|
delay += 200;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
@ -37,6 +37,16 @@ templ Upload(expirations []Expiry, uploadedLink string) {
|
|||||||
let drop_zone = document.getElementById("drop_zone");
|
let drop_zone = document.getElementById("drop_zone");
|
||||||
let desc = document.getElementById("desc");
|
let desc = document.getElementById("desc");
|
||||||
let fileInput = document.getElementById("files");
|
let fileInput = document.getElementById("files");
|
||||||
|
function updateDescription(len) {
|
||||||
|
desc.innerText = `${len} File`
|
||||||
|
if (len > 1) {
|
||||||
|
desc.innerText += "s"
|
||||||
|
}
|
||||||
|
desc.innerText += " Attached"
|
||||||
|
}
|
||||||
|
fileInput.addEventListener("change", () => {
|
||||||
|
updateDescription(fileInput.files.length)
|
||||||
|
});
|
||||||
drop_zone.addEventListener("click", function (ev) {
|
drop_zone.addEventListener("click", function (ev) {
|
||||||
fileInput.click();
|
fileInput.click();
|
||||||
})
|
})
|
||||||
@ -51,11 +61,7 @@ templ Upload(expirations []Expiry, uploadedLink string) {
|
|||||||
const newDT = new DataTransfer();
|
const newDT = new DataTransfer();
|
||||||
files.forEach(f => newDT.items.add(f));
|
files.forEach(f => newDT.items.add(f));
|
||||||
fileInput.files = newDT.files;
|
fileInput.files = newDT.files;
|
||||||
desc.innerText = `${fileInput.files.length} File`
|
updateDescription(fileInput.files.length)
|
||||||
if (fileInput.files.length > 1) {
|
|
||||||
desc.innerText += "s"
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
drop_zone.addEventListener("dragover", function (e) {
|
drop_zone.addEventListener("dragover", function (e) {
|
||||||
drop_zone.classList.add('dragover');
|
drop_zone.classList.add('dragover');
|
||||||
|
BIN
tmp/bin/main
Executable file
BIN
tmp/bin/main
Executable file
Binary file not shown.
1
tmp/build-errors.log
Normal file
1
tmp/build-errors.log
Normal file
@ -0,0 +1 @@
|
|||||||
|
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1
|
Loading…
x
Reference in New Issue
Block a user