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
54 changes: 54 additions & 0 deletions apps/buy.immich.app/+layout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<script>
import Icon from '$lib/components/icon.svelte';
import { mdiGithub, mdiWeb } from '@mdi/js';
import '$lib/app.css';
// document.documentElement.classList.add('dark');
</script>

<svelte:head>
<title>Immich - Purchase License</title>
<meta name="theme-color" content="currentColor" />
<meta name="description" content="Buy a license to support Immich" />

<!-- Facebook Meta Tags -->
<meta property="og:type" content="website" />
<meta property="og:title" content="Immich Licenses" />
<meta property="og:description" content="Buy a license to support Immich" />
<meta property="og:image" content="/img/social-preview.png" />

<!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Immich Licenses" />
<meta name="twitter:description" content="Buy a license to support Immich" />
<meta name="twitter:image" content="/img/social-preview.png" />
alextran1502 marked this conversation as resolved.
Show resolved Hide resolved
</svelte:head>

<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>
127 changes: 118 additions & 9 deletions apps/buy.immich.app/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,124 @@
<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 { onMount } from 'svelte';
import type { PageData } from './$types';
import { FUTO_ROUTES } from '$lib/utils/endpoints';
import LoadingSpinner from '$lib/components/loading-spinner.svelte';

export let data: PageData;

const redirectUrl = FUTO_ROUTES.paymentPortal;
const immichBuyBase = new URL('https://buy.immich.app');
const isRedirecting = data.productId && data.instanceUrl;

onMount(() => {
if (data.productId && data.instanceUrl) {
console.log('Navigating to FUTO Pay');

immichBuyBase.searchParams.append('instanceUrl', data.instanceUrl);

redirectUrl.searchParams.append('product', data.productId);
redirectUrl.searchParams.append('success', immichBuyBase.href);

setTimeout(() => {
window.location.href = redirectUrl.href;
}, 1000);
}
});
</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">
{#if isRedirecting}
<div class="flex flex-col gap-4 place-items-center place-content-center">
<LoadingSpinner />
<p>Redirecting you to the payment portal</p>
</div>
{:else}
<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={`${FUTO_ROUTES.paymentPortal}?product=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={`${FUTO_ROUTES.paymentPortal}?product=immich-client`}>
<Button fullwidth>Select</Button>
</a>
</div>
</div>
</div>
{/if}
</section>
</div>
30 changes: 30 additions & 0 deletions apps/buy.immich.app/+page.ts
Original file line number Diff line number Diff line change
@@ -1 +1,31 @@
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';

export const ssr = false;

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

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

if (orderId && instanceUrl) {
// http://10.1.15.216:5173/?instanceUrl=http%3A%2F%2F10.1.15.216%3A2283&orderId=c945fba0-088b-479b-b6af-0ec23d632108

const successUrl = new URL(`/success`, url.href);
successUrl.searchParams.append('instanceUrl', instanceUrl);
successUrl.searchParams.append('orderId', orderId);

redirect(308, successUrl);
alextran1502 marked this conversation as resolved.
Show resolved Hide resolved
}

return {
productId,
instanceUrl,
orderId,
};
}) satisfies PageLoad;
149 changes: 149 additions & 0 deletions apps/buy.immich.app/success/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<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 { mdiCheckCircleOutline } from '@mdi/js';
import type { PageData } from '../$types';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';

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

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

export let data: PageData;

let paymentStatus: PurchaseStatus = PurchaseStatus.Unknown;
let setIntervalHandler: number;
let setTimeoutHandler: number;
let redirectUrl: URL;

onMount(() => {
if (data.orderId && data.instanceUrl) {
setIntervalHandler = setInterval(() => {
getPurchaseStatus(data.orderId);
}, 5_000);

setTimeoutHandler = setTimeout(() => {
clearInterval(setIntervalHandler);
}, 30_000);
}

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

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

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

if (status.ok) {
const data = (await status.json()) as PaymentStatusResponseDto;

if (data.status === PurchaseStatus.Succeeded && data.purchaseId) {
paymentStatus = data.status;
clearTimers();
redirect(data.purchaseId);
}

if (data.status === PurchaseStatus.Failed) {
paymentStatus = data.status;
clearTimers();
}

if (data.status === PurchaseStatus.Pending) {
console.log('Purchase is still pending');
}
}
};

const redirect = (licenkeyKey: string) => {
redirectUrl = new URL('/buy', data.instanceUrl);
redirectUrl.searchParams.append('licenseKey', licenkeyKey);

setTimeout(() => {
window.location.href = redirectUrl.href;
}, 2000);
};
</script>

<svelte:head>
<title>Immich - Purchase Success</title>
<meta name="theme-color" content="currentColor" />
<meta name="description" content="Buy a license to support Immich" />

<!-- Facebook Meta Tags -->
<meta property="og:type" content="website" />
<meta property="og:title" content="Immich Licenses" />
<meta property="og:description" content="Buy a license to support Immich" />
<meta property="og:image" content="/img/social-preview.png" />

<!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Immich Licenses" />
<meta name="twitter:description" content="Buy a license to support Immich" />
<meta name="twitter:image" content="/img/social-preview.png" />
</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 paymentStatus === PurchaseStatus.Unknown}
<div class="bg-immich-bg rounded-full p-1 border">
<LoadingSpinner size="48" />
</div>
{/if}

{#if paymentStatus === PurchaseStatus.Succeeded}
<div in:fade={{ duration: 200 }}>
<Icon path={mdiCheckCircleOutline} size="48" class="text-white rounded-full bg-immich-primary" />
</div>
{/if}
</div>

{#if paymentStatus === PurchaseStatus.Unknown}
<p>Getting payment status</p>
{/if}

{#if paymentStatus === PurchaseStatus.Succeeded}
<p class="text-xl font-bold">Success</p>

<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={redirectUrl?.href}>
<button class="mt-2 p-4 bg-immich-primary text-white rounded-lg dark:text-black dark:bg-immich-dark-primary"
>Activate your instance</button
>
</a>
{/if}
</div>
</section>
</div>
10 changes: 10 additions & 0 deletions apps/buy.immich.app/success/+page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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;
return {
orderId,
instanceUrl,
};
}) satisfies PageLoad;
Loading