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