Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: content #11

Merged
merged 19 commits into from
Jul 16, 2024
36 changes: 36 additions & 0 deletions apps/buy.immich.app/+layout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script lang="ts">
import Icon from '$lib/components/icon.svelte';
import { mdiGithub, mdiWeb } from '@mdi/js';
import '$lib/app.css';
// document.documentElement.classList.add('dark');
</script>

<section class="grid grid-rows-[auto_60px] h-auto lg:h-screen">
<slot />

<div class="flex flex-col place-content-center bg-gray-50 dark:bg-immich-dark-bg dark:text-white">
<div class="flex place-items-center place-content-center gap-4">
<a
class="underline flex gap-1 place-items-center"
href="https://github.com/immich-app/immich"
target="_blank"
rel="noopener noreferrer"
>
<Icon path={mdiGithub} />
GitHub</a
>
<a
class="underline flex gap-1 place-items-center"
href="https://immich.app"
target="_blank"
rel="noopener noreferrer"
>
<Icon path={mdiWeb} />
Documentation</a
>
<a class="underline" href="https://discord.immich.app/" target="_blank" rel="noopener noreferrer"> Discord</a>

<a class="underline" href="https://futo.org/" target="_blank" rel="noopener noreferrer">FUTO</a>
</div>
</div>
</section>
1 change: 1 addition & 0 deletions apps/buy.immich.app/+layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ssr = false;
96 changes: 87 additions & 9 deletions apps/buy.immich.app/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,93 @@
<script lang="ts">
import '$lib/app.css';
import Button from '$lib/components/button.svelte';
import Icon from '$lib/components/icon.svelte';
import { mdiCheckCircleOutline, mdiServer, mdiAccount } from '@mdi/js';
import { getRedirectUrl } from '$lib/utils/endpoints';
</script>

<div class="w-screen h-screen bg-immich-dark-bg overflow-auto p-4">
<div class="mx-auto max-w-screen-sm m-6 p-12 rounded-[50px] bg-immich-dark-gray text-immich-dark-fg">
<section class="flex justify-center">
<img src="/img/immich-logo-stacked-dark.svg" class="h-64" alt="Immich logo" />
</section>
<section>
<h1 class="md:text-3xl mb-2 text-immich-dark-primary">Buy Immich</h1>
<p class="mb-4">Buy Immich!</p>
</section>
<div class="w-full h-full md:max-w-[900px] px-4 py-10 sm:px-20 lg:p-10 m-auto">
<div class="m-auto">
<h1 class="text-4xl font-bold text-immich-primary dark:text-immich-dark-primary tracking-wider">LICENSE</h1>
<p class="text-lg mt-2 dark:text-immich-gray">Buy a license to support Immich</p>
</div>

<section class="flex justify-center mt-6">
<img src="/img/social-preview.png" alt="Sociel Preview" class="rounded-3xl" />
</section>

<section class="mt-10">
<div class="flex gap-6 mt-4 justify-between flex-wrap lg:flex-nowrap">
<!-- SERVER LICENSE -->
<div
class="border border-gray-300 dark:border-gray-800 w-full p-8 rounded-3xl bg-gray-100 dark:bg-gray-900 hover:bg-immich-primary/10 dark:hover:bg-immich-primary/20 transition-all"
>
<div class="text-immich-primary dark:text-immich-dark-primary">
<Icon path={mdiServer} size="56" />
<p class="font-semibold text-lg mt-1">Server License</p>
</div>

<div class="mt-4 dark:text-immich-gray">
<p class="text-6xl font-bold">$99<span class="text-2xl font-medium">.99</span></p>
<p>per server</p>
</div>

<div class="flex flex-col justify-between h-[200px] dark:text-immich-gray">
<div class="mt-6 flex flex-col gap-1">
<div class="grid grid-cols-[36px_auto]">
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
<p class="self-center">1 license per server</p>
</div>

<div class="grid grid-cols-[36px_auto]">
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
<p class="self-center">Lifetime license</p>
</div>

<div class="grid grid-cols-[36px_auto]">
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
<p class="self-center">License for all users on the server</p>
</div>
</div>

<a href={getRedirectUrl('immich-server')}>
<Button fullwidth>Select</Button>
</a>
</div>
</div>

<!-- USER LICENSE -->
<div
class="border border-gray-300 dark:border-gray-800 w-full p-8 rounded-3xl bg-gray-100 dark:bg-gray-900 hover:bg-immich-primary/10 dark:hover:bg-immich-primary/20"
>
<div class="text-immich-primary dark:text-immich-dark-primary">
<Icon path={mdiAccount} size="56" />
<p class="font-semibold text-lg mt-1">Individual License</p>
</div>

<div class="mt-4 dark:text-immich-gray">
<p class="text-6xl font-bold">$24<span class="text-2xl font-medium">.99</span></p>
<p>per user</p>
</div>

<div class="flex flex-col justify-between h-[200px] dark:text-immich-gray">
<div class="mt-6 flex flex-col gap-1">
<div class="grid grid-cols-[36px_auto]">
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
<p class="self-center">1 license per user on any server</p>
</div>

<div class="grid grid-cols-[36px_auto]">
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
<p class="self-center">Lifetime license</p>
</div>
</div>

<a href={getRedirectUrl('immich-client')}>
<Button fullwidth>Select</Button>
</a>
</div>
</div>
</div>
</section>
</div>
23 changes: 22 additions & 1 deletion apps/buy.immich.app/+page.ts
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
export const ssr = false;
import { getRedirectUrl } from '$lib/utils/endpoints';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';

enum ProductType {
Server = 'immich-server',
Inidividual = 'immich-client',
}

export const load = (({ url }) => {
const productId = url.searchParams.get('productId') as ProductType;
const instanceUrl = url.searchParams.get('instanceUrl') as string;

if (productId && instanceUrl) {
redirect(302, getRedirectUrl(productId, instanceUrl));
}

return {
productId,
instanceUrl,
};
}) satisfies PageLoad;
165 changes: 165 additions & 0 deletions apps/buy.immich.app/success/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
<script lang="ts">
import Icon from '$lib/components/icon.svelte';
import LoadingSpinner from '$lib/components/loading-spinner.svelte';
import { FUTO_ROUTES } from '$lib/utils/endpoints';
import { mdiAlertCircleOutline, mdiCheckCircleOutline } from '@mdi/js';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';

import type { PageData } from './$types';

enum PurchaseStatus {
Pending = -1,
Failed = 0,
Succeeded = 1,
Unknown = 2,
}

type PaymentStatusResponseDto = {
status: PurchaseStatus;
purchaseId?: string; // License key
};

export let data: PageData;
let setIntervalHandler: number;
let isLoading = true;
let response: PaymentStatusResponseDto = { status: PurchaseStatus.Unknown };

onMount(() => {
return () => {
clearTimers();
};
});

const clearTimers = () => {
clearInterval(setIntervalHandler);
};

const getRedirectUrl = (licenseKey: string, instanceUrl: string) => {
const redirectUrl = new URL('/link?target=activate_license', instanceUrl);
redirectUrl.searchParams.append('licenseKey', licenseKey);

return redirectUrl.href;
};

const getPurchaseStatus = async (orderId: string) => {
const status = await fetch(new URL(orderId, FUTO_ROUTES.getPaymentStatus));

if (!status.ok) {
return;
}

response = (await status.json()) as PaymentStatusResponseDto;

switch (response.status) {
case PurchaseStatus.Failed:
isLoading = false;
break;
case PurchaseStatus.Succeeded:
isLoading = false;
if (data.instanceUrl && response.purchaseId) {
const url = getRedirectUrl(response.purchaseId, data.instanceUrl);

setTimeout(() => (window.location.href = url), 2000);
}
break;
default:
break;
}
};

getPurchaseStatus(data.orderId);

setIntervalHandler = setInterval(() => {
if (isLoading) {
getPurchaseStatus(data.orderId);
} else {
clearTimers();
}
}, 5_000);

setTimeout(() => (isLoading = false), 30_000);
</script>

<svelte:head>
<title>Immich - Purchase Success</title>
</svelte:head>

<div class="w-full h-full md:max-w-[900px] px-4 py-10 sm:px-20 lg:p-10 m-auto">
<div class="m-auto">
<h1 class="text-4xl font-bold text-immich-primary dark:text-immich-dark-primary tracking-wider">PURCHASE STATUS</h1>
<p class="text-lg mt-2 dark:text-immich-gray">Processing your purchase</p>
</div>

<section class="flex justify-center mt-6">
<img src="/img/social-preview.png" alt="Sociel Preview" class="rounded-3xl" />
</section>

<section class="mt-10">
<div
class="flex gap-4 flex-col place-content-center place-items-center text-center mt-4 justify-between relative border p-10 rounded-3xl bg-gray-100"
>
<div class="absolute -top-[24px] left-[calc(50%-24px)]">
{#if isLoading}
<div class="bg-immich-bg rounded-full p-1 border">
<LoadingSpinner size="48" />
</div>
{:else}
<!-- abc -->
{#if response.status === PurchaseStatus.Succeeded}
<div in:fade={{ duration: 200 }}>
<Icon path={mdiCheckCircleOutline} size="48" class="text-white rounded-full bg-immich-primary" />
</div>
{:else}
<div in:fade={{ duration: 200 }}>
<Icon path={mdiAlertCircleOutline} size="48" class="text-white rounded-full bg-red-500" />
</div>
{/if}
{/if}
</div>

{#if isLoading}
<p>Getting payment status</p>
{:else}
{#if response.status === PurchaseStatus.Pending}
<p>
Purchase is still pending, please check your email after a few minutes for the license key. Payment can take
up to 48 hours in some cases, depending on payment provider
</p>
{/if}

{#if response.status === PurchaseStatus.Failed || response.status === PurchaseStatus.Unknown}
<p>Fail to get payment status, please check your email for more details</p>
{/if}

{#if response.status === PurchaseStatus.Succeeded}
{#if response.purchaseId}
{#if data.instanceUrl}
<div class="flex gap-2 place-items-center place-content-center">
<LoadingSpinner />
<p>Redirecting back to your instance, click on the button below if you aren't navigated back</p>
</div>

<a href={getRedirectUrl(response.purchaseId, data.instanceUrl)}>
<button
class="mt-2 p-4 bg-immich-primary text-white rounded-full dark:text-black dark:bg-immich-dark-primary hover:shadow-xl"
>Activate your instance</button
>
</a>
{:else}
<p class="text-lg font-bold">Your License Key</p>
<div class="bg-immich-primary/10 text-immich-primary py-4 px-8 rounded-lg">{response.purchaseId}</div>
<a href={getRedirectUrl(response.purchaseId, 'https://my.immich.app')}>
<button
class="mt-2 p-4 bg-immich-primary text-white rounded-full dark:text-black dark:bg-immich-dark-primary hover:shadow-xl"
>Activate your instance</button
>
</a>
<p class="text-sm mt-4">The license key is also sent to the email you provided</p>
{/if}
{/if}
{/if}
{/if}
</div>
</section>
</div>
16 changes: 16 additions & 0 deletions apps/buy.immich.app/success/+page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';

export const load = (async ({ url }) => {
const orderId = url.searchParams.get('orderId') as string;
const instanceUrl = url.searchParams.get('instanceUrl') as string;

if (!orderId) {
redirect(302, '/');
}

return {
orderId,
instanceUrl,
};
}) satisfies PageLoad;
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,8 @@
"vite": "^5.0.3",
"vitest": "^1.2.0"
},
"type": "module"
"type": "module",
"dependencies": {
"@mdi/js": "^7.4.47"
}
}
Loading