Add affiliate prompt branding option

This commit is contained in:
2025-03-21 12:57:53 +01:00
parent d894246a38
commit 7aff2a2ead
7 changed files with 312 additions and 150 deletions

View File

@@ -70,6 +70,7 @@ let toolbar_toggle = () => {
let downloader
let list_updater
let details_window
let affiliate_prompt
let details_visible = false
let qr_window
let qr_visible = false
@@ -256,6 +257,9 @@ const apply_customizations = file => {
if (file.branding.footer_link) {
custom_footer_link = file.branding.footer_link
}
if (file.branding.affiliate_prompt) {
affiliate_prompt.prompt(file.branding.affiliate_prompt)
}
if (file.branding.disable_download_button && !file.can_edit) {
disable_download_button = true
}
@@ -619,7 +623,7 @@ const keyboard_event = evt => {
<!-- At the bottom so it renders over everything else -->
<LoadingIndicator loading={loading}/>
<AffiliatePrompt/>
<AffiliatePrompt bind:this={affiliate_prompt}/>
</div>
<style>

View File

@@ -3,10 +3,18 @@ export let on = false
export let group_first = false
export let group_middle = false
export let group_last = false
export let action = (e: MouseEvent) => {}
const click = (e: MouseEvent) => {
on = !on
if (typeof action === "function") {
action(e)
}
}
</script>
<button
on:click={() => on = !on}
on:click={click}
type="button"
class="button"
class:button_highlight={on}

View File

@@ -0,0 +1,107 @@
// Response types
// ==============
export type GenericResponse = {
value: string,
message: string,
}
export type User = {
username: string,
email: string,
subscription: Subscription,
storage_space_used: number,
filesystem_storage_used: number,
is_admin: boolean,
balance_micro_eur: number,
hotlinking_enabled: boolean,
monthly_transfer_cap: number,
monthly_transfer_used: number,
file_viewer_branding: Map<string, string>,
file_embed_domains: string,
skip_file_viewer: boolean,
affiliate_user_name: string,
}
export type Subscription = {
id: string,
name: string,
type: string,
file_size_limit: number,
file_expiry_days: number,
storage_space: number,
price_per_tb_storage: number,
price_per_tb_bandwidth: number,
monthly_transfer_cap: number,
file_viewer_branding: boolean,
filesystem_access: boolean,
filesystem_storage_limit: number,
}
// Utility funcs
// =============
export const get_endpoint = () => {
if ((window as any).api_endpoint !== undefined) {
return (window as any).api_endpoint as string
}
console.warn("api_endpoint property is not defined on window")
return "/api"
}
export const check_response = async (resp: Response) => {
let text = await resp.text()
if (resp.status >= 400) {
let error: any
try {
error = JSON.parse(text) as GenericResponse
} catch (err) {
error = text
}
throw error
}
return JSON.parse(text)
}
export const dict_to_form = (dict: Object) => {
let form = new FormData()
for (let key of Object.keys(dict)) {
if (dict[key] === undefined) {
continue
} else if (dict[key] instanceof Date) {
form.append(key, new Date(dict[key]).toISOString())
} else if (typeof dict[key] === "object") {
form.append(key, JSON.stringify(dict[key]))
} else {
form.append(key, dict[key])
}
}
return form
}
// API methods
// ===========
export const get_user = async () => {
if ((window as any).user !== undefined) {
return (window as any).user as User
}
console.warn("user property is not defined on window")
return await check_response(await fetch(get_endpoint() + "/user")) as User
}
export const put_user = async (data: Object) => {
check_response(await fetch(
get_endpoint() + "/user",
{ method: "PUT", body: dict_to_form(data) },
))
// Update the window.user variable
for (let key of Object.keys(data)) {
((window as any).user as User)[key] = data[key]
}
}

View File

@@ -1,8 +1,18 @@
<script>
import { onMount } from "svelte";
import CopyButton from "../layout/CopyButton.svelte";
import Form from "./../util/Form.svelte";
import Button from "../layout/Button.svelte";
let affiliate_link = window.location.protocol+"//"+window.location.host + "?ref=" + window.user.username
let affiliate_link = window.location.protocol+"//"+window.location.host + "?ref=" + encodeURIComponent(window.user.username)
let affiliate_deny = false
onMount(() => {
affiliate_deny = localStorage.getItem("affiliate_deny") === "1"
})
const enable_affiliate_prompt = () => {
affiliate_deny = false
localStorage.removeItem("affiliate_deny")
}
let account_settings = {
name: "account_settings",
@@ -144,15 +154,27 @@ let delete_account = {
Your own affiliate link is
<a href="{affiliate_link}">{affiliate_link}</a>
<CopyButton small_icon text={affiliate_link}/>. Share this link
with premium pixeldrain users to gain commissions. For a
detailed description of the affiliate program please check out
the <a href="/about#toc_12">Q&A page</a>.
with premium pixeldrain users to gain commissions. You can use
the "?ref={encodeURIComponent(window.user.username)}" referral
code on download pages too. For a detailed description of the
affiliate program please check out the <a
href="/about#toc_12">Q&A page</a>.
</p>
<p>
Note that the link includes the name of your pixeldrain
account. If you change your account name the link will stop
working and you might stop receiving commissions.
</p>
{#if affiliate_deny}
<div class="highlight_blue">
You currently have affiliate prompts disabled. You will not
see affiliate requests from other users. If you wish to
enable it again, click here:
<br/>
<Button click={enable_affiliate_prompt} label="Enable affiliate prompts"/>
</div>
{/if}
</div>
</fieldset>

View File

@@ -1,22 +1,35 @@
<script>
<script lang="ts">
import { onMount } from "svelte";
import Modal from "../util/Modal.svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte";
import { get_user, put_user } from "../lib/PixeldrainAPI.mjs";
// When the always flag is set then the pop-up will also show if the user
// already has an affiliate ID set
export let always = false
let modal
let loading
let ref
let modal: Modal
let loading: boolean
let referral: string
let shown = false
export const prompt = async (ref: string) => {
referral = ref
const user = await get_user()
if (referral === null) {
return
} else if (referral === user.affiliate_user_name) {
return // User is already supporting this affiliate ID
} else if (referral === user.username) {
return // This is your own referral link
}
onMount(() => {
if (!always) {
if (window.user.subscription.id === "") {
if (user.subscription.id === "") {
// User does not have an active subscription, setting referral will
// not have effect
return
} else if (window.user.affiliate_user_name !== "") {
} else if (user.affiliate_user_name !== "") {
// User is already sponsoring someone
return
} else if (localStorage.getItem("affiliate_deny") === "1") {
@@ -25,28 +38,21 @@ onMount(() => {
}
}
ref = new URLSearchParams(document.location.search).get("ref")
if (ref === null) {
// The prompt can only be shown once per page. This should prevent it from
// showing up every time someone loads a new file.
if (shown === true) {
return
} else if (ref === window.user.affiliate_user_name) {
return // User is already supporting this affiliate ID
}
shown = true
modal.show()
})
}
onMount(() => prompt(new URLSearchParams(document.location.search).get("ref")))
const allow = async () => {
loading = true
try {
const form = new FormData()
form.append("affiliate_user_name", ref)
const resp = await fetch(window.api_endpoint+"/user", { method: "PUT", body: form });
if(resp.status >= 400) {
throw (await resp.json()).message
}
// Update the window.user variable
window.user.affiliate_user_name = ref
await put_user({affiliate_user_name: referral})
// Close the popup
modal.hide()
@@ -67,10 +73,10 @@ const deny = () => {
<LoadingIndicator bind:loading={loading} />
<section>
<p>
Hi! {ref} wants you to sponsor their pixeldrain account. This will
give them €0.50 every month in pixeldrain prepaid credit. They can
use this credit to get a discount on their file sharing and storage
efforts. Here is a short summary of what this entails:
Hi! {referral} wants you to sponsor their pixeldrain account. This
will give them €0.50 every month in pixeldrain prepaid credit. They
can use this credit to get a discount on their file storage and
sharing costs. Here is a short summary of what this entails:
</p>
<ul>
<li>

View File

@@ -6,6 +6,7 @@ import SuccessMessage from "../util/SuccessMessage.svelte";
import ThemePicker from "../util/ThemePicker.svelte";
import { onMount } from "svelte";
import Persistence from "../icons/Persistence.svelte";
import ToggleButton from "../layout/ToggleButton.svelte";
let loading = false
let success_message
@@ -19,6 +20,7 @@ let header_link = ""
let background_image = ""
let footer_image = ""
let footer_link = ""
let affiliate_prompt = false
let disable_download_button = false
let disable_share_button = false
@@ -59,6 +61,9 @@ let save = async () => {
form.append("footer_link", footer_link)
form.append("disable_download_button", disable_download_button)
form.append("disable_share_button", disable_share_button)
if (affiliate_prompt) {
form.append("affiliate_prompt", window.user.username)
}
try {
const resp = await fetch(
@@ -90,6 +95,7 @@ onMount(() => {
background_image = b.background_image ? b.background_image : ""
footer_image = b.footer_image ? b.footer_image : ""
footer_link = b.footer_link ? b.footer_link : ""
affiliate_prompt = b.affiliate_prompt === window.user.username ? true : false
disable_download_button = b.disable_download_button ? b.disable_download_button : false
disable_share_button = b.disable_share_button ? b.disable_share_button : false
}
@@ -105,12 +111,6 @@ onMount(() => {
Sharing settings are not available for your account. Subscribe to
the Persistence plan or higher to enable these features.
</div>
{:else if !window.user.hotlinking_enabled}
<div class="highlight_yellow">
To use custom file viewer branding hotlinking needs to be
enabled. Enable hotlinking on the
<a href="/user/sharing/bandwidth">sharing settings page</a>.
</div>
{/if}
<SuccessMessage bind:this={success_message}></SuccessMessage>
@@ -125,122 +125,135 @@ onMount(() => {
should use APNG or WebP. Avoid using animated GIFs as they are very slow
to load.
</p>
<h3>Theme</h3>
<p>
Choose a theme for your download pages. This theme will override the
theme preference of the person viewing the file. Set to 'None' to let
the viewer choose their own theme.
</p>
<ThemePicker
theme={theme}
on:theme_change={e => {theme = e.detail; save()}}>
</ThemePicker>
<fieldset>
<legend>Theme</legend>
<p>
Choose a theme for your download pages. This theme will override the
theme preference of the person viewing the file. Set to 'None' to let
the viewer choose their own theme.
</p>
<ThemePicker
theme={theme}
on:theme_change={e => {theme = e.detail; save()}}>
</ThemePicker>
</fieldset>
<h3>Header image</h3>
<p>
Will be shown above the file. Maximum height is 90px. Will be shrunk if
larger. You can also add a link to open when the visitor clicks the
image. The link needs to start with 'https://'.
</p>
<button on:click={() => {select_file("header")}}>
<i class="icon">add_photo_alternate</i>
Select header image
</button>
<button on:click={() => {header_image = ""; save()}}>
<i class="icon">close</i>
Remove
</button>
<br/>
Header image link:<br/>
<form class="form_row" on:submit|preventDefault={save}>
<input class="grow" bind:value={header_link} type="text" placeholder="https://"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form>
<fieldset>
<legend>Header image</legend>
<p>
Will be shown above the file. Maximum height is 90px. Will be shrunk if
larger. You can also add a link to open when the visitor clicks the
image. The link needs to start with 'https://'.
</p>
<button on:click={() => {select_file("header")}}>
<i class="icon">add_photo_alternate</i>
Select header image
</button>
<button on:click={() => {header_image = ""; save()}}>
<i class="icon">close</i>
Remove
</button>
<br/>
Header image link:<br/>
<form class="form_row" on:submit|preventDefault={save}>
<input class="grow" bind:value={header_link} type="text" placeholder="https://"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form>
{#if header_image}
<div class="highlight_shaded">
<CustomBanner src={"/api/file/"+header_image} link={header_link}></CustomBanner>
</div>
{/if}
<h3>Background image</h3>
<p>
This image will be shown behind the file which is being viewed. I
recommend choosing something dark and not too distracting. Try to keep
the file below 1 MB to not harm page loading times. Using a JPEG image
with a quality value of 60 is usually good enough.
</p>
<button on:click={() => {select_file("background")}}>
<i class="icon">add_photo_alternate</i>
Select background image
</button>
<button on:click={() => {background_image = ""; save()}}>
<i class="icon">close</i>
Remove
</button>
{#if background_image}
<div class="highlight_shaded">
<img class="background_preview" src="/api/file/{background_image}" alt="Custom file viewer background"/>
</div>
{/if}
<h3>Footer image</h3>
<p>
Will be shown below the file. Maximum height is 90px. Will be shrunk if
larger.
</p>
<button on:click={() => {select_file("footer")}}>
<i class="icon">add_photo_alternate</i>
Select footer image
</button>
<button on:click={() => {footer_image = ""; save()}}>
<i class="icon">close</i>
Remove
</button>
<br/>
Footer image link:<br/>
<form class="form_row" on:submit|preventDefault={save}>
<input class="grow" bind:value={footer_link} type="text" placeholder="https://"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form>
{#if footer_image}
<div class="highlight_shaded">
<CustomBanner src={"/api/file/"+footer_image} link={footer_link}></CustomBanner>
</div>
{/if}
<h3>Toolbar buttons</h3>
<p>
If you don't want to make it obvious that your files can be downloaded
or shared while still allowing people to view them through the site you
can use these options.
</p>
<p>
The buttons will be hidden, however your files can still be downloaded
and shared through the API. The changes are purely cosmetic.
</p>
<p>
For convenience these options only apply when other people view your
files. The buttons are still available to you. If you want to see the
effects you can open your file in an incognito window.
</p>
Disable download button:
<button on:click={() => {disable_download_button = !disable_download_button; save()}}>
{#if disable_download_button}
<i class="icon">check</i> ON (click to turn off)
{:else}
<i class="icon">close</i> OFF (click to turn on)
{#if header_image}
<div class="highlight_shaded">
<CustomBanner src={"/api/file/"+header_image} link={header_link}></CustomBanner>
</div>
{/if}
</button>
<br/>
Disable share button:
<button on:click={() => {disable_share_button = !disable_share_button; save()}}>
{#if disable_share_button}
<i class="icon">check</i> ON (click to turn off)
{:else}
<i class="icon">close</i> OFF (click to turn on)
</fieldset>
<fieldset>
<legend>Background image</legend>
<p>
This image will be shown behind the file which is being viewed. I
recommend choosing something dark and not too distracting. Try to keep
the file below 1 MB to not harm page loading times. Using a JPEG image
with a quality value of 60 is usually good enough.
</p>
<button on:click={() => {select_file("background")}}>
<i class="icon">add_photo_alternate</i>
Select background image
</button>
<button on:click={() => {background_image = ""; save()}}>
<i class="icon">close</i>
Remove
</button>
{#if background_image}
<div class="highlight_shaded">
<img class="background_preview" src="/api/file/{background_image}" alt="Custom file viewer background"/>
</div>
{/if}
</button>
</fieldset>
<fieldset>
<legend>Footer image</legend>
<p>
Will be shown below the file. Maximum height is 90px. Will be shrunk if
larger.
</p>
<button on:click={() => {select_file("footer")}}>
<i class="icon">add_photo_alternate</i>
Select footer image
</button>
<button on:click={() => {footer_image = ""; save()}}>
<i class="icon">close</i>
Remove
</button>
<br/>
Footer image link:<br/>
<form class="form_row" on:submit|preventDefault={save}>
<input class="grow" bind:value={footer_link} type="text" placeholder="https://"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form>
{#if footer_image}
<div class="highlight_shaded">
<CustomBanner src={"/api/file/"+footer_image} link={footer_link}></CustomBanner>
</div>
{/if}
</fieldset>
<fieldset>
<legend>Affiliate prompt</legend>
<p>
When this is enabled premium users on your download pages will be
asked to support you through pixeldrain's <a
href="/about#toc_12">affiliate program</a>.
</p>
<ToggleButton bind:on={affiliate_prompt} action={save}>
Enable affiliate prompt
</ToggleButton>
</fieldset>
<fieldset>
<legend>Toolbar buttons</legend>
<p>
If you don't want to make it obvious that your files can be downloaded
or shared while still allowing people to view them through the site you
can use these options.
</p>
<p>
The buttons will be hidden, however your files can still be downloaded
and shared through the API. The changes are purely cosmetic.
</p>
<p>
For convenience these options only apply when other people view your
files. The buttons are still available to you. If you want to see the
effects you can open your file in an incognito window.
</p>
<ToggleButton bind:on={disable_download_button} action={save}>
Disable download button
</ToggleButton>
<br/>
<ToggleButton bind:on={disable_share_button} action={save}>
Disable share button
</ToggleButton>
</fieldset>
</section>
<FilePicker

View File

@@ -141,6 +141,7 @@ onMount(() => {
</ul>
</div>
</div>
<div>
<div class="feat_label" class:feat_highlight={subscription === "prepaid"}>
Prepaid<br/>
@@ -188,6 +189,7 @@ onMount(() => {
</ul>
</div>
</div>
<div>
<div class="feat_label" class:feat_highlight={subscription === ""}>
Free<br/>