Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions apps/frontend/src/composables/affiliates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const useAffiliates = () => {
const affiliateCookie = useCookie('mrs_afl', {
// maxAge: 60 * 60 * 24 * 7, // 7 days
maxAge: 60 * 60, // an hour
sameSite: 'lax',
secure: true,
httpOnly: false,
path: '/',
})

const setAffiliateCode = (code: string) => {
affiliateCookie.value = code
}

const getAffiliateCode = (): string | undefined => {
return affiliateCookie.value || undefined
}

return {
setAffiliateCode,
getAffiliateCode,
}
}
28 changes: 26 additions & 2 deletions apps/frontend/src/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,12 @@
link: '/admin/user_email',
shown: isAdmin(auth.user),
},
{
id: 'affiliates',
color: 'primary',
link: '/admin/affiliates',
shown: isAdmin(auth.user),
},
{
id: 'servers-notices',
color: 'primary',
Expand All @@ -399,14 +405,17 @@
<ReportIcon aria-hidden="true" /> {{ formatMessage(messages.reports) }}
</template>
<template #user-lookup>
<UserIcon aria-hidden="true" /> {{ formatMessage(messages.lookupByEmail) }}
<UserSearchIcon aria-hidden="true" /> {{ formatMessage(messages.lookupByEmail) }}
</template>
<template #file-lookup>
<FileIcon aria-hidden="true" /> {{ formatMessage(messages.fileLookup) }}
</template>
<template #servers-notices>
<IssuesIcon aria-hidden="true" /> {{ formatMessage(messages.manageServerNotices) }}
</template>
<template #affiliates>
<AffiliateIcon aria-hidden="true" /> {{ formatMessage(messages.manageAffiliates) }}
</template>
</OverflowMenu>
</ButtonStyled>
<ButtonStyled type="transparent">
Expand Down Expand Up @@ -483,6 +492,10 @@
<template #organizations>
<OrganizationIcon aria-hidden="true" /> {{ formatMessage(messages.organizations) }}
</template>
<template #affiliate-links>
<AffiliateIcon aria-hidden="true" />
{{ formatMessage(commonMessages.affiliateLinksButton) }}
</template>
<template #revenue>
<CurrencyIcon aria-hidden="true" /> {{ formatMessage(messages.revenue) }}
</template>
Expand Down Expand Up @@ -770,6 +783,7 @@
</template>
<script setup>
import {
AffiliateIcon,
ArrowBigUpDashIcon,
BellIcon,
BlueskyIcon,
Expand Down Expand Up @@ -809,6 +823,7 @@ import {
SunIcon,
TwitterIcon,
UserIcon,
UserSearchIcon,
XIcon,
} from '@modrinth/assets'
import {
Expand All @@ -821,7 +836,7 @@ import {
OverflowMenu,
PagewideBanner,
} from '@modrinth/ui'
import { isAdmin, isStaff } from '@modrinth/utils'
import { isAdmin, isStaff, UserBadge } from '@modrinth/utils'
import { IntlFormatted } from '@vintl/vintl/components'

import TextLogo from '~/components/brand/TextLogo.vue'
Expand Down Expand Up @@ -1055,6 +1070,10 @@ const messages = defineMessages({
id: 'layout.action.manage-server-notices',
defaultMessage: 'Manage server notices',
},
manageAffiliates: {
id: 'layout.action.manage-affiliates',
defaultMessage: 'Manage affiliate links',
},
newProject: {
id: 'layout.action.new-project',
defaultMessage: 'New project',
Expand Down Expand Up @@ -1232,6 +1251,11 @@ const userMenuOptions = computed(() => {
id: 'organizations',
link: '/dashboard/organizations',
},
{
id: 'affiliate-links',
link: '/dashboard/affiliate-links',
shown: auth.value.user.badges & UserBadge.AFFILIATE,
},
{
id: 'revenue',
link: '/dashboard/revenue',
Expand Down
3 changes: 3 additions & 0 deletions apps/frontend/src/locales/en-US/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,9 @@
"layout.action.lookup-by-email": {
"message": "Lookup by email"
},
"layout.action.manage-affiliates": {
"message": "Manage affiliates"
},
"layout.action.manage-server-notices": {
"message": "Manage server notices"
},
Expand Down
190 changes: 190 additions & 0 deletions apps/frontend/src/pages/admin/affiliates.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<template>
<NewModal ref="createModal" header="Creating new affiliate code">
<div class="flex flex-col gap-4">
<span class="text-lg font-semibold text-contrast">Modrinth username of affiliate</span>
<div class="flex items-center gap-2">
<div class="iconified-input">
<UserIcon aria-hidden="true" />
<input
v-model="affiliateUsername"
class="card-shadow"
autocomplete="off"
spellcheck="false"
type="text"
:placeholder="`Username`"
/>
<Button v-if="affiliateUsername" class="r-btn" @click="() => (affiliateUsername = '')">
<XIcon />
</Button>
</div>
<ButtonStyled color="brand">
<button @click="createAffiliateCode">
<PlusIcon />
Create affiliate code
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
<ConfirmModal
ref="revokeModal"
:title="`Are you sure you want to revoke ${revokingAffiliateUsername}'s affiliate code?`"
:description="`This will permanently revoke the affiliate code \`${revokingAffiliateId}\` and make any links that this user has shared invalid.`"
:proceed-icon="XCircleIcon"
:proceed-label="`Revoke`"
@proceed="confirmRevokeAffiliateCode"
/>
<div class="page">
<div
class="mb-6 flex items-center gap-6 border-0 border-b-[1px] border-solid border-divider pb-6"
>
<h1 class="m-0 grow text-2xl font-extrabold">Manage affiliate links</h1>
<div class="flex items-center gap-2">
<div class="iconified-input">
<SearchIcon aria-hidden="true" />
<input
v-model="filterQuery"
class="card-shadow"
autocomplete="off"
spellcheck="false"
type="text"
:placeholder="`Search affiliates...`"
/>
<Button v-if="filterQuery" class="r-btn" @click="() => (filterQuery = '')">
<XIcon />
</Button>
</div>
<ButtonStyled color="brand">
<button @click="createModal?.show">
<PlusIcon />
Create affiliate
</button>
</ButtonStyled>
</div>
</div>
<div
v-for="affiliate in filteredAffiliates"
:key="`affiliate-${affiliate.id}`"
class="card-shadow mb-3 flex items-center gap-4 rounded-2xl bg-bg-raised p-4"
>
<nuxt-link
:to="`/user/${affiliate.username}`"
tabindex="-1"
class="flex gap-4 items-center hover:brightness-[--hover-brightness] w-fit text-lg font-bold text-contrast hover:underline"
>
<Avatar :src="affiliate.avatar_url" circle size="48px" />
{{ affiliate.username }}
</nuxt-link>
<div class="ml-auto flex items-center gap-2">
<ButtonStyled>
<nuxt-link>
<ChartIcon />
View analytics
</nuxt-link>
</ButtonStyled>
<ButtonStyled color="red" color-fill="text">
<button @click="revokeAffiliateCode(affiliate.username, affiliate.id)">
<XCircleIcon /> Remove affiliate
</button>
</ButtonStyled>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ChartIcon, PlusIcon, SearchIcon, UserIcon, XCircleIcon, XIcon } from '@modrinth/assets'
import { Avatar, Button, ButtonStyled, ConfirmModal, NewModal } from '@modrinth/ui'

const createModal = useTemplateRef<typeof NewModal>('createModal')
const revokeModal = useTemplateRef<typeof ConfirmModal>('revokeModal')

const { data: affiliates, error, refresh } = useAsyncData('affiliates', () => useBaseFetch('affiliate/admin', { internal: true })).then(({ data }) => console.log(data))


const filterQuery = ref('')

const filteredAffiliates = computed(() =>
affiliateUsers.value.filter((affiliate) =>
filterQuery.value.trim()
? affiliate.username.trim().toLowerCase().includes(filterQuery.value.trim().toLowerCase())
: true,
),
)

// placeholder affiliate data
const affiliateUsers = computed(() => {
return [
{
id: 'Dc7EYhxG',
username: 'Prospector',
avatar_url:
'https://cdn.modrinth.com/user/Dc7EYhxG/32e8b1f7d18288262d1ed92cbdf43272d21b4fcd.png',
bio: 'Software Engineer at Modrinth.\n\nFounder of ModFest, co-founder of TerraformersMC. Creator of Traverse and Mod Menu.',
created: '2020-11-06T04:56:05.014379Z',
role: 'admin',
badges: 1,
auth_providers: null,
email: null,
email_verified: null,
has_password: null,
has_totp: null,
payout_data: null,
stripe_customer_id: null,
allow_friend_requests: null,
github_id: null,
affiliate_codes: ['cD2EThlR'],
},
{
id: 'MpxzqsyW',
username: 'Geometrically',
avatar_url:
'https://cdn.modrinth.com/data/MpxzqsyW/eb0038489a55e7e7a188a5b50462f0b10dfc1613_96.webp',
bio: 'I make stuff',
created: '2020-10-19T02:30:03.202550Z',
role: 'admin',
badges: 1,
auth_providers: null,
email: null,
email_verified: null,
has_password: null,
has_totp: null,
payout_data: null,
stripe_customer_id: null,
allow_friend_requests: null,
github_id: null,
affiliate_codes: ['p4DcS4lQ'],
},
]
})

const affiliateUsername = ref<string | null>(null)

function createAffiliateCode() {
createModal.value?.hide()
affiliateUsername.value = null
}

const revokingAffiliateUsername = ref<string | null>(null)
const revokingAffiliateId = ref<string | null>(null)

function revokeAffiliateCode(username: string, affiliateId: string) {
revokingAffiliateUsername.value = username
revokingAffiliateId.value = affiliateId
revokeModal.value?.show()
}

function confirmRevokeAffiliateCode() {
revokeModal.value?.hide()
revokingAffiliateUsername.value = null
revokingAffiliateId.value = null
}
</script>

<style lang="scss" scoped>
.page {
padding: 1rem;
margin-left: auto;
margin-right: auto;
max-width: 78.5rem;
}
</style>
17 changes: 16 additions & 1 deletion apps/frontend/src/pages/dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
>
<LibraryIcon aria-hidden="true" />
</NavStackItem>
<NavStackItem
v-if="isAffiliate"
link="/dashboard/affiliate-links"
:label="formatMessage(commonMessages.affiliateLinksButton)"
>
<AffiliateIcon aria-hidden="true" />
</NavStackItem>
<NavStackItem link="/dashboard/revenue" label="Revenue">
<CurrencyIcon aria-hidden="true" />
</NavStackItem>
Expand All @@ -41,8 +48,9 @@
</div>
</div>
</template>
<script setup>
<script setup lang="ts">
import {
AffiliateIcon,
BellIcon as NotificationsIcon,
ChartIcon,
CurrencyIcon,
Expand All @@ -53,10 +61,17 @@ import {
ReportIcon,
} from '@modrinth/assets'
import { commonMessages } from '@modrinth/ui'
import { type User,UserBadge } from '@modrinth/utils'

import NavStack from '~/components/ui/NavStack.vue'
import NavStackItem from '~/components/ui/NavStackItem.vue'

const auth = (await useAuth()) as Ref<{ user: User | null }>

const isAffiliate = computed(() => {
return auth.value.user && (auth.value.user.badges & UserBadge.AFFILIATE)
})

const { formatMessage } = useVIntl()

definePageMeta({
Expand Down
Loading
Loading