Affiliates frontend (#4380)

* Begin affiliates frontend

* Significant work on hooking up affiliates ui

* Clean up server nodes menu

* affiliates work

* update affiliate time

* oops

* fix local import

* fix local import x2

* remove line in dashboard

* lint
This commit is contained in:
Prospector
2025-11-02 11:32:18 -08:00
committed by GitHub
parent b7f0988399
commit 40cbe92dbc
33 changed files with 1202 additions and 37 deletions

1
packages/assets/external/facebook.svg vendored Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><title>Facebook</title><path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"/></svg>

After

Width:  |  Height:  |  Size: 563 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><title>Instagram</title><path d="M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

3
packages/assets/external/reels.svg vendored Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 122.14 122.88" fill="currentColor">
<path d="M35.14 0H87c9.65 0 18.43 3.96 24.8 10.32 6.38 6.37 10.34 15.16 10.34 24.82v52.61c0 9.64-3.96 18.42-10.32 24.79l-.02.02c-6.38 6.37-15.16 10.32-24.79 10.32H35.14c-9.66 0-18.45-3.96-24.82-10.32l-.24-.27C3.86 105.95 0 97.27 0 87.74v-52.6c0-9.67 3.95-18.45 10.32-24.82S25.47 0 35.14 0zm56.37 31.02.07.11h21.6c-.87-5.68-3.58-10.78-7.48-14.69-4.8-4.8-11.42-7.78-18.7-7.78h-8.87l13.38 22.36zm-9.99.11L68.07 8.66h-29.5l13.61 22.47h29.34zm-39.41 0L28.95 9.39a26.446 26.446 0 0 0-12.51 7.05c-3.9 3.9-6.6 9.01-7.48 14.69h33.15zm71.37 8.66H8.66v47.96c0 7.17 2.89 13.7 7.56 18.48l.22.21c4.8 4.8 11.43 7.79 18.7 7.79H87c7.28 0 13.9-2.98 18.69-7.77l.02-.02c4.79-4.79 7.77-11.41 7.77-18.69V39.79zM50.95 54.95 77.78 72.4c.43.28.82.64 1.13 1.08a3.9 3.9 0 0 1-1 5.42L51.19 94.67c-.67.55-1.53.88-2.48.88a3.91 3.91 0 0 1-3.91-3.91V58.15h.02a3.902 3.902 0 0 1 6.13-3.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 978 B

1
packages/assets/external/snapchat.svg vendored Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><title>Snapchat</title><path d="M12.206.793c.99 0 4.347.276 5.93 3.821.529 1.193.403 3.219.299 4.847l-.003.06c-.012.18-.022.345-.03.51.075.045.203.09.401.09.3-.016.659-.12 1.033-.301.165-.088.344-.104.464-.104.182 0 .359.029.509.09.45.149.734.479.734.838.015.449-.39.839-1.213 1.168-.089.029-.209.075-.344.119-.45.135-1.139.36-1.333.81-.09.224-.061.524.12.868l.015.015c.06.136 1.526 3.475 4.791 4.014.255.044.435.27.42.509 0 .075-.015.149-.045.225-.24.569-1.273.988-3.146 1.271-.059.091-.12.375-.164.57-.029.179-.074.36-.134.553-.076.271-.27.405-.555.405h-.03c-.135 0-.313-.031-.538-.074-.36-.075-.765-.135-1.273-.135-.3 0-.599.015-.913.074-.6.104-1.123.464-1.723.884-.853.599-1.826 1.288-3.294 1.288-.06 0-.119-.015-.18-.015h-.149c-1.468 0-2.427-.675-3.279-1.288-.599-.42-1.107-.779-1.707-.884-.314-.045-.629-.074-.928-.074-.54 0-.958.089-1.272.149-.211.043-.391.074-.54.074-.374 0-.523-.224-.583-.42-.061-.192-.09-.389-.135-.567-.046-.181-.105-.494-.166-.57-1.918-.222-2.95-.642-3.189-1.226-.031-.063-.052-.15-.055-.225-.015-.243.165-.465.42-.509 3.264-.54 4.73-3.879 4.791-4.02l.016-.029c.18-.345.224-.645.119-.869-.195-.434-.884-.658-1.332-.809-.121-.029-.24-.074-.346-.119-1.107-.435-1.257-.93-1.197-1.273.09-.479.674-.793 1.168-.793.146 0 .27.029.383.074.42.194.789.3 1.104.3.234 0 .384-.06.465-.105l-.046-.569c-.098-1.626-.225-3.651.307-4.837C7.392 1.077 10.739.807 11.727.807l.419-.015h.06z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
packages/assets/external/threads.svg vendored Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><title>Threads</title><path d="M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 0 1 3.02.142c-.126-.742-.375-1.332-.75-1.757-.513-.586-1.308-.883-2.359-.89h-.029c-.844 0-1.992.232-2.721 1.32L7.734 7.847c.98-1.454 2.568-2.256 4.478-2.256h.044c3.194.02 5.097 1.975 5.287 5.388.108.046.216.094.321.142 1.49.7 2.58 1.761 3.154 3.07.797 1.82.871 4.79-1.548 7.158-1.85 1.81-4.094 2.628-7.277 2.65Zm1.003-11.69c-.242 0-.487.007-.739.021-1.836.103-2.98.946-2.916 2.143.067 1.256 1.452 1.839 2.784 1.767 1.224-.065 2.818-.543 3.086-3.71a10.5 10.5 0 0 0-2.215-.221z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
packages/assets/external/tiktok.svg vendored Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><title>TikTok</title><path d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"/></svg>

After

Width:  |  Height:  |  Size: 728 B

1
packages/assets/external/twitch.svg vendored Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><title>Twitch</title><path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z"/></svg>

After

Width:  |  Height:  |  Size: 313 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><title>YouTube Gaming</title><path d="M24 13.2v-6l-6-3.6-6 3.6-6-3.6-6 3.6v6l12 7.2zM8.4 10.8H6v2.4H4.8v-2.4H2.4V9.6h2.4V7.2H6v2.4h2.4zm7.2 2.4a1.2 1.2 0 01-1.2-1.2c0-.66.54-1.2 1.2-1.2.66 0 1.2.54 1.2 1.2 0 .66-.54 1.2-1.2 1.2zm3.6-2.4A1.2 1.2 0 0118 9.6c0-.66.54-1.2 1.2-1.2.66 0 1.2.54 1.2 1.2 0 .66-.54 1.2-1.2 1.2Z"/></svg>

After

Width:  |  Height:  |  Size: 420 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><title>YouTube Shorts</title><path d="m18.931 9.99-1.441-.601 1.717-.913a4.48 4.48 0 0 0 1.874-6.078 4.506 4.506 0 0 0-6.09-1.874L4.792 5.929a4.504 4.504 0 0 0-2.402 4.193 4.521 4.521 0 0 0 2.666 3.904c.036.012 1.442.6 1.442.6l-1.706.901a4.51 4.51 0 0 0-2.369 3.967A4.528 4.528 0 0 0 6.93 24c.725 0 1.437-.174 2.08-.508l10.21-5.406a4.494 4.494 0 0 0 2.39-4.192 4.525 4.525 0 0 0-2.678-3.904ZM9.597 15.19V8.824l6.007 3.184z"/></svg>

After

Width:  |  Height:  |  Size: 523 B

View File

@@ -1,6 +1,7 @@
// Auto-generated icon imports and exports
// Do not edit this file manually - run 'pnpm run fix' to regenerate
import _AffiliateIcon from './icons/affiliate.svg?component'
import _AlignLeftIcon from './icons/align-left.svg?component'
import _ArchiveIcon from './icons/archive.svg?component'
import _ArrowBigRightDashIcon from './icons/arrow-big-right-dash.svg?component'
@@ -30,6 +31,7 @@ import _CheckCheckIcon from './icons/check-check.svg?component'
import _CheckCircleIcon from './icons/check-circle.svg?component'
import _ChevronLeftIcon from './icons/chevron-left.svg?component'
import _ChevronRightIcon from './icons/chevron-right.svg?component'
import _CircleUserIcon from './icons/circle-user.svg?component'
import _ClearIcon from './icons/clear.svg?component'
import _ClientIcon from './icons/client.svg?component'
import _ClipboardCopyIcon from './icons/clipboard-copy.svg?component'
@@ -190,6 +192,7 @@ import _UploadIcon from './icons/upload.svg?component'
import _UserIcon from './icons/user.svg?component'
import _UserCogIcon from './icons/user-cog.svg?component'
import _UserPlusIcon from './icons/user-plus.svg?component'
import _UserSearchIcon from './icons/user-search.svg?component'
import _UserXIcon from './icons/user-x.svg?component'
import _UsersIcon from './icons/users.svg?component'
import _VersionIcon from './icons/version.svg?component'
@@ -202,6 +205,7 @@ import _XCircleIcon from './icons/x-circle.svg?component'
import _ZoomInIcon from './icons/zoom-in.svg?component'
import _ZoomOutIcon from './icons/zoom-out.svg?component'
export const AffiliateIcon = _AffiliateIcon
export const AlignLeftIcon = _AlignLeftIcon
export const ArchiveIcon = _ArchiveIcon
export const ArrowBigRightDashIcon = _ArrowBigRightDashIcon
@@ -231,6 +235,7 @@ export const CheckCircleIcon = _CheckCircleIcon
export const CheckIcon = _CheckIcon
export const ChevronLeftIcon = _ChevronLeftIcon
export const ChevronRightIcon = _ChevronRightIcon
export const CircleUserIcon = _CircleUserIcon
export const ClearIcon = _ClearIcon
export const ClientIcon = _ClientIcon
export const ClipboardCopyIcon = _ClipboardCopyIcon
@@ -390,6 +395,7 @@ export const UpdatedIcon = _UpdatedIcon
export const UploadIcon = _UploadIcon
export const UserCogIcon = _UserCogIcon
export const UserPlusIcon = _UserPlusIcon
export const UserSearchIcon = _UserSearchIcon
export const UserXIcon = _UserXIcon
export const UserIcon = _UserIcon
export const UsersIcon = _UsersIcon

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<g>
<circle cx="12" cy="12" r="7"/>
<circle cx="12" cy="10.6" r="2.1"/>
<circle cx="3.7" cy="3.7" r="1.8"/>
<circle cx="20.3" cy="3.7" r="1.8"/>
<circle cx="3.7" cy="20.3" r="1.8"/>
<circle cx="20.3" cy="20.3" r="1.8"/>
<path d="M8.5 18.1v-1.2c0-.8.6-1.4 1.4-1.4h4.2c.8 0 1.4.6 1.4 1.4v1.2M17 7l2-2M5 19l2-2M17 17l2 2M5 5l2 2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 532 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-user-icon lucide-circle-user"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/></svg>

After

Width:  |  Height:  |  Size: 368 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-search-icon lucide-user-search"><circle cx="10" cy="7" r="4"/><path d="M10.3 15H7a4 4 0 0 0-4 4v2"/><circle cx="17" cy="17" r="3"/><path d="m21 21-1.9-1.9"/></svg>

After

Width:  |  Height:  |  Size: 370 B

View File

@@ -23,29 +23,38 @@ import _SleepingRinthbot from './branding/rinthbot/sleeping.webp'
import _SobbingRinthbot from './branding/rinthbot/sobbing.webp'
import _ThinkingRinthbot from './branding/rinthbot/thinking.webp'
import _WavingRinthbot from './branding/rinthbot/waving.webp'
// External Icons
import _AppleIcon from './external/apple.svg?component'
import _BlueskyIcon from './external/bluesky.svg?component'
import _BuyMeACoffeeIcon from './external/bmac.svg?component'
import _CurseForgeIcon from './external/curseforge.svg?component'
import _DiscordIcon from './external/discord.svg?component'
import _FacebookIcon from './external/facebook.svg?component'
import _GithubIcon from './external/github.svg?component'
import _InstagramIcon from './external/instagram.svg?component'
import _KoFiIcon from './external/kofi.svg?component'
import _MastodonIcon from './external/mastodon.svg?component'
import _OpenCollectiveIcon from './external/opencollective.svg?component'
import _PatreonIcon from './external/patreon.svg?component'
import _PayPalIcon from './external/paypal.svg?component'
import _RedditIcon from './external/reddit.svg?component'
// External Icons
import _ReelsIcon from './external/reels.svg?component'
import _SnapchatIcon from './external/snapchat.svg?component'
import _SSODiscordIcon from './external/sso/discord.svg?component'
import _SSOGitHubIcon from './external/sso/github.svg?component'
import _SSOGitLabIcon from './external/sso/gitlab.svg?component'
import _SSOGoogleIcon from './external/sso/google.svg?component'
import _SSOMicrosoftIcon from './external/sso/microsoft.svg?component'
import _SSOSteamIcon from './external/sso/steam.svg?component'
import _ThreadsIcon from './external/threads.svg?component'
import _TikTokIcon from './external/tiktok.svg?component'
import _TumblrIcon from './external/tumblr.svg?component'
import _TwitchIcon from './external/twitch.svg?component'
import _TwitterIcon from './external/twitter.svg?component'
import _WindowsIcon from './external/windows.svg?component'
import _YouTubeIcon from './external/youtube.svg?component'
import _YouTubeGaming from './external/youtubegaming.svg?component'
import _YouTubeShortsIcon from './external/youtubeshorts.svg?component'
export const ModrinthIcon = _ModrinthIcon
export const BrowserWindowSuccessIllustration = _BrowserWindowSuccessIllustration
@@ -73,6 +82,13 @@ export const BuyMeACoffeeIcon = _BuyMeACoffeeIcon
export const GithubIcon = _GithubIcon
export const CurseForgeIcon = _CurseForgeIcon
export const DiscordIcon = _DiscordIcon
export const FacebookIcon = _FacebookIcon
export const InstagramIcon = _InstagramIcon
export const SnapchatIcon = _SnapchatIcon
export const ReelsIcon = _ReelsIcon
export const TikTokIcon = _TikTokIcon
export const TwitchIcon = _TwitchIcon
export const ThreadsIcon = _ThreadsIcon
export const KoFiIcon = _KoFiIcon
export const MastodonIcon = _MastodonIcon
export const OpenCollectiveIcon = _OpenCollectiveIcon
@@ -83,6 +99,8 @@ export const TumblrIcon = _TumblrIcon
export const TwitterIcon = _TwitterIcon
export const WindowsIcon = _WindowsIcon
export const YouTubeIcon = _YouTubeIcon
export const YouTubeGaming = _YouTubeGaming
export const YouTubeShortsIcon = _YouTubeShortsIcon
export * from './generated-icons'
export { default as ClassicPlayerModel } from './models/classic-player.gltf?url'

View File

@@ -0,0 +1,72 @@
<template>
<div class="card-shadow flex flex-col gap-4 rounded-2xl bg-bg-raised p-4">
<div class="flex items-center gap-4">
<div
class="flex items-center justify-center rounded-xl border-[1px] border-solid border-button-border bg-button-bg p-2"
>
<AutoBrandIcon :keyword="affiliate.source_name" class="h-6 w-6">
<AffiliateIcon />
</AutoBrandIcon>
</div>
<div class="flex flex-col">
<span class="w-fit text-lg font-bold text-contrast">
{{ affiliate.source_name }}
</span>
<span v-if="createdBy" class="text-sm text-secondary">
{{ formatMessage(messages.createdBy, { user: createdBy }) }}
</span>
</div>
<div class="ml-auto flex items-center gap-2">
<slot />
<ButtonStyled v-if="showRevoke" color="red" color-fill="text">
<button @click="emit('revoke', affiliate)">
<XCircleIcon />
{{ formatMessage(messages.revokeAffiliateLink) }}
</button>
</ButtonStyled>
</div>
</div>
<CopyCode :text="`https://modrinth.gg?afl=${affiliate.id}`" />
</div>
</template>
<script setup lang="ts">
import { AffiliateIcon, XCircleIcon } from '@modrinth/assets'
import type { AffiliateLink } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { AutoBrandIcon, ButtonStyled, CopyCode } from '../index.ts'
withDefaults(
defineProps<{
affiliate: AffiliateLink
showRevoke?: boolean
createdBy?: string
}>(),
{
showRevoke: true,
createdBy: undefined,
},
)
const emit = defineEmits<{
(e: 'revoke', affiliate: AffiliateLink): void
}>()
const { formatMessage } = useVIntl()
const messages = defineMessages({
viewAnalytics: {
id: 'affiliate.viewAnalytics',
defaultMessage: 'View analytics',
},
revokeAffiliateLink: {
id: 'affiliate.revoke',
defaultMessage: 'Revoke affiliate link',
},
createdBy: {
id: 'affiliate.createdBy',
defaultMessage: 'Created by {user}',
},
})
</script>

View File

@@ -0,0 +1,165 @@
<template>
<NewModal ref="modal" :header="formatMessage(messages.createHeader)">
<div class="flex flex-col">
<label v-if="showUserField" class="contents" for="create-affiliate-user-input">
<span class="text-lg font-semibold text-contrast mb-1">
{{ formatMessage(messages.createUserLabel) }}
</span>
<span class="text-secondary mb-2">{{ formatMessage(messages.createUserDescription) }}</span>
</label>
<div v-if="showUserField" class="mb-4">
<div class="iconified-input">
<UserIcon aria-hidden="true" />
<input
id="create-affiliate-user-input"
v-model="affiliateUsername"
class="card-shadow"
autocomplete="off"
spellcheck="false"
type="text"
:placeholder="formatMessage(messages.createUserPlaceholder)"
/>
<Button v-if="affiliateUsername" class="r-btn" @click="() => (affiliateUsername = '')">
<XIcon />
</Button>
</div>
</div>
<label class="contents" for="create-affiliate-title-input">
<span class="text-lg font-semibold text-contrast mb-1">
{{ formatMessage(messages.createTitleLabel) }}
</span>
<span class="text-secondary mb-2">{{
formatMessage(messages.createTitleDescription)
}}</span>
</label>
<div class="flex items-center gap-2">
<div class="iconified-input">
<AutoBrandIcon :keyword="affiliateLinkTitle" aria-hidden="true">
<AffiliateIcon />
</AutoBrandIcon>
<input
id="create-affiliate-title-input"
v-model="affiliateLinkTitle"
class="card-shadow"
autocomplete="off"
spellcheck="false"
type="text"
:placeholder="formatMessage(messages.createTitlePlaceholder)"
/>
<Button v-if="affiliateLinkTitle" class="r-btn" @click="() => (affiliateLinkTitle = '')">
<XIcon />
</Button>
</div>
<ButtonStyled color="brand">
<button :disabled="creatingLink || !canCreate" @click="createAffiliateLink">
<SpinnerIcon v-if="creatingLink" class="animate-spin" />
<PlusIcon v-else />
{{ formatMessage(creatingLink ? messages.creatingButton : messages.createButton) }}
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
</template>
<script lang="ts"></script>
<script setup lang="ts">
import { AffiliateIcon, PlusIcon, SpinnerIcon, UserIcon, XIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { computed, ref, useTemplateRef } from 'vue'
import { AutoBrandIcon, Button, ButtonStyled, NewModal } from '../index.ts'
export type CreateAffiliateProps = { sourceName: string; username?: string }
const props = withDefaults(
defineProps<{
showUserField?: boolean
creatingLink?: boolean
}>(),
{
showUserField: false,
creatingLink: false,
},
)
const emit = defineEmits<{
(e: 'create', data: CreateAffiliateProps): void
}>()
const modal = useTemplateRef<typeof NewModal>('modal')
const { formatMessage } = useVIntl()
const affiliateLinkTitle = ref('')
const affiliateUsername = ref('')
const canCreate = computed(() => {
if (props.showUserField) {
return affiliateLinkTitle.value.trim() && affiliateUsername.value.trim()
}
return affiliateLinkTitle.value.trim()
})
function createAffiliateLink() {
if (!canCreate.value) {
return
}
emit('create', {
sourceName: affiliateLinkTitle.value,
username: props.showUserField ? affiliateUsername.value : undefined,
})
}
function close() {
modal.value?.hide()
affiliateLinkTitle.value = ''
affiliateUsername.value = ''
}
function show() {
modal.value?.show()
}
defineExpose({
show,
close,
})
const messages = defineMessages({
createHeader: {
id: 'affiliate.create.header',
defaultMessage: 'Creating new affiliate code',
},
createTitleLabel: {
id: 'affiliate.create.title.label',
defaultMessage: 'Title of affiliate link',
},
createTitleDescription: {
id: 'affiliate.create.title.description',
defaultMessage: 'Give your affiliate link a name so you know where people are coming from!',
},
createTitlePlaceholder: {
id: 'affiliate.create.title.placeholder',
defaultMessage: 'e.g. YouTube',
},
createUserLabel: {
id: 'affiliate.create.user.label',
defaultMessage: 'Username',
},
createUserDescription: {
id: 'affiliate.create.user.description',
defaultMessage: 'The username of the user to create the affiliate code for',
},
createUserPlaceholder: {
id: 'affiliate.create.user.placeholder',
defaultMessage: 'Enter username...',
},
createButton: {
id: 'affiliate.create.button',
defaultMessage: 'Create affiliate link',
},
creatingButton: {
id: 'affiliate.creating.button',
defaultMessage: 'Creating affiliate link...',
},
})
</script>

View File

@@ -0,0 +1,153 @@
<script setup lang="ts">
import {
AppleIcon,
BlueskyIcon,
BuyMeACoffeeIcon,
CurseForgeIcon,
DiscordIcon,
FacebookIcon,
GithubIcon,
InstagramIcon,
KoFiIcon,
MastodonIcon,
ModrinthIcon,
OpenCollectiveIcon,
PatreonIcon,
PayPalIcon,
RedditIcon,
ReelsIcon,
SnapchatIcon,
ThreadsIcon,
TikTokIcon,
TumblrIcon,
TwitchIcon,
TwitterIcon,
WindowsIcon,
YouTubeGaming,
YouTubeIcon,
YouTubeShortsIcon,
} from '@modrinth/assets'
import { computed } from 'vue'
const props = defineProps<{
keyword: string
}>()
const services = [
{
icon: AppleIcon,
keywords: ['apple'],
},
{
icon: BlueskyIcon,
keywords: ['bluesky', 'bsky', 'blue sky'],
},
{
icon: BuyMeACoffeeIcon,
keywords: ['buymeacoffee', 'bmac', 'buy me a coffee'],
},
{
icon: DiscordIcon,
keywords: ['discord'],
},
{
icon: FacebookIcon,
keywords: ['facebook', 'fb', 'face book'],
},
{
icon: GithubIcon,
keywords: ['github', 'gh', 'git hub'],
},
{
icon: ThreadsIcon,
keywords: ['threads'],
},
{
icon: InstagramIcon,
keywords: ['instagram', 'ig', 'insta'],
},
{
icon: KoFiIcon,
keywords: ['ko-fi', 'kofi', 'ko fi'],
},
{
icon: MastodonIcon,
keywords: ['mastodon'],
},
{
icon: OpenCollectiveIcon,
keywords: ['opencollective', 'open collective'],
},
{
icon: PatreonIcon,
keywords: ['patreon'],
},
{
icon: PayPalIcon,
keywords: ['paypal', 'pay pal'],
},
{
icon: RedditIcon,
keywords: ['reddit'],
},
{
icon: ReelsIcon,
keywords: ['reels', 'instagram reels', 'facebook reels'],
},
{
icon: SnapchatIcon,
keywords: ['snapchat'],
},
{
icon: TikTokIcon,
keywords: ['tiktok', 'tik', 'tok'],
},
{
icon: TumblrIcon,
keywords: ['tumblr'],
},
{
icon: TwitchIcon,
keywords: ['twitch', 'twitch.tv'],
},
{
icon: WindowsIcon,
keywords: ['windows', 'microsoft'],
},
{
icon: YouTubeIcon,
keywords: ['youtube', 'yt'],
},
{
icon: YouTubeShortsIcon,
keywords: ['shorts', 'youtube shorts'],
},
{
icon: YouTubeGaming,
keywords: ['youtube gaming'],
},
{
icon: CurseForgeIcon,
keywords: ['curseforge', 'cf', 'curse', 'curse forge'],
},
{
icon: ModrinthIcon,
keywords: ['modrinth', 'mod rinth', 'modrith', 'mr'],
},
{
icon: TwitterIcon,
keywords: ['twitter', 'x.com', 'x'],
},
]
const selectedService = computed(() =>
services.find((service) =>
service.keywords.some((keyword) => props.keyword.toLowerCase().includes(keyword)),
),
)
</script>
<template>
<component :is="selectedService?.icon" v-if="selectedService" />
<slot v-else />
</template>

View File

@@ -58,6 +58,7 @@ const props = defineProps<{
) => Promise<UpdatePaymentIntentResponse | CreatePaymentIntentResponse | null>
onError: (err: Error) => void
onFinalizeNoPaymentChange?: () => Promise<void>
affiliateCode?: string | null
}>()
const modal = useTemplateRef<InstanceType<typeof NewModal>>('modal')
@@ -66,6 +67,7 @@ const selectedInterval = ref<ServerBillingInterval>('quarterly')
const loading = ref(false)
const selectedRegion = ref<string>()
const projectId = ref<string>()
const affiliateCode = ref(props.affiliateCode ?? null)
const {
initializeStripe,
@@ -96,6 +98,7 @@ const {
projectId,
props.initiatePayment,
props.onError,
affiliateCode,
)
const customServer = ref<boolean>(false)

View File

@@ -2,6 +2,7 @@
export { default as Accordion } from './base/Accordion.vue'
export { default as Admonition } from './base/Admonition.vue'
export { default as AppearingProgressBar } from './base/AppearingProgressBar.vue'
export { default as AutoBrandIcon } from './base/AutoBrandIcon.vue'
export { default as AutoLink } from './base/AutoLink.vue'
export { default as Avatar } from './base/Avatar.vue'
export { default as Badge } from './base/Badge.vue'
@@ -98,6 +99,10 @@ export { default as SearchFilterControl } from './search/SearchFilterControl.vue
export { default as SearchFilterOption } from './search/SearchFilterOption.vue'
export { default as SearchSidebarFilter } from './search/SearchSidebarFilter.vue'
// Affiliate
export { default as AffiliateLinkCard } from './affiliate/AffiliateLinkCard.vue'
export { default as AffiliateLinkCreateModal } from './affiliate/AffiliateLinkCreateModal.vue'
// Billing
export { default as AddPaymentMethodModal } from './billing/AddPaymentMethodModal.vue'
export { default as ModrinthServersPurchaseModal } from './billing/ModrinthServersPurchaseModal.vue'

View File

@@ -37,6 +37,7 @@ export const useStripe = (
body: CreatePaymentIntentRequest | UpdatePaymentIntentRequest,
) => Promise<CreatePaymentIntentResponse | UpdatePaymentIntentResponse | null>,
onError: (err: Error) => void,
affiliateCode?: Ref<string | null>,
) => {
const stripe = ref<StripeJs | null>(null)
@@ -229,6 +230,13 @@ export const useStripe = (
let result: BasePaymentIntentResponse | null = null
const affiliateMetadata =
affiliateCode && affiliateCode.value
? {
affiliate_code: affiliateCode.value,
}
: {}
const metadata: CreatePaymentIntentRequest['metadata'] = {
type: 'pyro',
server_region: region.value,
@@ -237,6 +245,7 @@ export const useStripe = (
project_id: project.value,
}
: {},
...affiliateMetadata,
}
if (paymentIntentId.value) {

View File

@@ -1,4 +1,40 @@
{
"affiliate.create.button": {
"defaultMessage": "Create affiliate link"
},
"affiliate.create.header": {
"defaultMessage": "Creating new affiliate code"
},
"affiliate.create.title.description": {
"defaultMessage": "Give your affiliate link a name so you know where people are coming from!"
},
"affiliate.create.title.label": {
"defaultMessage": "Title of affiliate link"
},
"affiliate.create.title.placeholder": {
"defaultMessage": "e.g. YouTube"
},
"affiliate.create.user.description": {
"defaultMessage": "The username of the user to create the affiliate code for"
},
"affiliate.create.user.label": {
"defaultMessage": "Username"
},
"affiliate.create.user.placeholder": {
"defaultMessage": "Enter username..."
},
"affiliate.createdBy": {
"defaultMessage": "Created by {user}"
},
"affiliate.creating.button": {
"defaultMessage": "Creating affiliate link..."
},
"affiliate.revoke": {
"defaultMessage": "Revoke affiliate link"
},
"affiliate.viewAnalytics": {
"defaultMessage": "View analytics"
},
"badge.beta": {
"defaultMessage": "Beta"
},
@@ -8,6 +44,9 @@
"badge.new": {
"defaultMessage": "New"
},
"button.affiliate-links": {
"defaultMessage": "Affiliate links"
},
"button.analytics": {
"defaultMessage": "Analytics"
},

View File

@@ -79,6 +79,7 @@ export type CreatePaymentIntentRequest = PaymentRequestType & {
type: 'pyro'
server_name?: string
server_region?: string
affiliate_code?: string
source:
| {
loader: Loaders

View File

@@ -1,6 +1,10 @@
import { defineMessages } from '@vintl/vintl'
export const commonMessages = defineMessages({
affiliateLinksButton: {
id: 'button.affiliate-links',
defaultMessage: 'Affiliate links',
},
analyticsButton: {
id: 'button.analytics',
defaultMessage: 'Analytics',

View File

@@ -334,6 +334,7 @@ export enum UserBadge {
ALPHA_TESTER = 1 << 4,
CONTRIBUTOR = 1 << 5,
TRANSLATOR = 1 << 6,
AFFILIATE = 1 << 7,
}
export type UserBadges = number
@@ -597,3 +598,11 @@ export interface DelphiReport {
status: 'pending' | 'approved' | 'rejected'
content?: string
}
export type AffiliateLink = {
id: string
created_at: string
created_by: string
affiliate: string
source_name: string
}