New organizations (#1488)

* [WIP] Transfer organizations to own branch

* push progress

* Setup organizations page

* Add organizations grid to user profile

* Remove debug

* Add error handling for failed organization fetch

* Refactor organization page and settings

* Restructure to composition setup api

* checklist completion

* Apply suggestions from code review

Co-authored-by: Emma Alexia <emma@modrinth.com>

* Update pages/[type]/[id]/settings/index.vue

Co-authored-by: Emma Alexia <emma@modrinth.com>

* Update pages/[type]/[id]/settings/index.vue

Co-authored-by: Emma Alexia <emma@modrinth.com>

* Update pages/[type]/[id]/settings/index.vue

Co-authored-by: Emma Alexia <emma@modrinth.com>

* Update pages/[type]/[id]/settings/index.vue

Co-authored-by: Emma Alexia <emma@modrinth.com>

* Clean up org state management

* Refactor useClientTry to simplify code

* Remove unused code and update dependencies

* Refactor bulkEditLinks event handler

* Refactor organization management functions

* Update heading from "Creators" to "Members"

* Refactor team member invitation

* Refactor member management functions

* Implement validation on clientside for org names

* Name sanitization for fun characters

* Update onInviteTeamMember function parameters

* Remove name

* sidebar

* random rendering issue

* Conform to org removal

* Org no projects conditional

* Update organization links in dashboard

* Update Cards to universal-cards

* Refactor gallery upload permissions

* Refactor to sidebar pattern

* Update button classes in gallery and versions components

* Finish (most)

* almost finish

* Finish orgs :D

* Fix lint

* orgs fixes

* fix most things

* project settings

* convert grid to cards

* clean up unused test class

* Settings -> Manage

* add org view to org management

* Fix prop mounting issue

* fix analytics grid layout overflow

* fix multiselect breaking layout

* Refactor chart selection logic in ChartDisplay.vue

* Add transfer modal

---------

Co-authored-by: Jai A <jaiagr+gpg@pm.me>
Co-authored-by: Emma Alexia <emma@modrinth.com>
This commit is contained in:
Carter
2024-01-06 15:09:26 -08:00
committed by GitHub
parent 1108b0264e
commit d893765b24
44 changed files with 4092 additions and 1037 deletions

View File

@@ -115,6 +115,11 @@ const orderedCollections = computed(() => {
.collections-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
@media screen and (max-width: 800px) {
grid-template-columns: repeat(1, 1fr);
}
gap: var(--gap-md);
.collection {

View File

@@ -1,51 +0,0 @@
<template>
<div v-if="user.follows.length > 0" class="project-list display-mode--list">
<ProjectCard
v-for="project in user.follows"
:id="project.id"
:key="project.id"
:type="project.project_type"
:categories="project.categories"
:created-at="project.published"
:updated-at="project.updated"
:description="project.description"
:downloads="project.downloads ? project.downloads.toString() : '0'"
:icon-url="project.icon_url"
:name="project.title"
:client-side="project.client_side"
:server-side="project.server_side"
:color="project.color"
:show-updated-date="false"
>
<button class="iconified-button" @click="userUnfollowProject(project)">
<HeartIcon />
Unfollow
</button>
</ProjectCard>
</div>
<div v-else class="error">
<FollowIllustration class="icon" />
<br />
<span class="text"
>You don't have any followed projects. <br />
Why don't you <nuxt-link class="link" to="/mods">search</nuxt-link> for new ones?</span
>
</div>
</template>
<script setup>
import ProjectCard from '~/components/ui/ProjectCard.vue'
import HeartIcon from 'assets/images/utils/heart.svg'
import FollowIllustration from 'assets/images/illustrations/follow_illustration.svg'
const user = await useUser()
if (process.client) {
await initUserFollows()
}
useHead({ title: 'Followed review - Modrinth' })
definePageMeta({
middleware: 'auth',
})
</script>

View File

@@ -0,0 +1,198 @@
<template>
<div>
<OrganizationCreateModal ref="createOrgModal" />
<section class="universal-card">
<div class="header__row">
<h2 class="header__title">Organizations</h2>
<div class="input-group">
<button class="iconified-button brand-button" @click="openCreateOrgModal">
<PlusIcon />
Create organization
</button>
</div>
</div>
<template v-if="orgs?.length > 0">
<div class="orgs-grid">
<nuxt-link
v-for="org in orgs"
:key="org.id"
:to="`/organization/${org.slug}`"
class="universal-card button-base recessed org"
:class="{ 'is-disabled': org.members?.length === 0 }"
>
<Avatar :src="org.icon_url" :alt="org.name" class="icon" />
<div class="details">
<div class="title">
{{ org.name }}
</div>
<div class="description">
{{ org.description }}
</div>
<span class="stat-bar">
<div class="stats">
<UsersIcon />
{{ org.members?.length || 0 }} member
<template v-if="org.members?.length !== 1">s</template>
</div>
</span>
</div>
</nuxt-link>
</div>
</template>
<template v-else> Make an organization! </template>
</section>
</div>
</template>
<script setup>
import { PlusIcon, Avatar, UsersIcon } from 'omorphia'
import { useAuth } from '~/composables/auth.js'
import OrganizationCreateModal from '~/components/ui/OrganizationCreateModal.vue'
const createOrgModal = ref(null)
const auth = await useAuth()
const uid = computed(() => auth.value.user?.id || null)
const { data: orgs, error } = useAsyncData('organizations', () => {
if (!uid.value) return Promise.resolve(null)
return useBaseFetch('user/' + uid.value + '/organizations', {
apiVersion: 3,
})
})
if (error.value) {
createError({
statusCode: 500,
message: 'Failed to fetch organizations',
})
}
const openCreateOrgModal = () => {
createOrgModal.value?.show()
}
</script>
<style scoped lang="scss">
.project-meta-item {
display: flex;
flex-direction: column;
justify-content: flex-start;
padding: var(--spacing-card-sm);
.project-title {
margin-bottom: var(--spacing-card-sm);
}
}
.orgs-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
@media screen and (max-width: 750px) {
grid-template-columns: repeat(1, 1fr);
}
gap: var(--gap-md);
.org {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--gap-md);
margin-bottom: 0;
.icon {
width: 100% !important;
height: 6rem !important;
max-width: unset !important;
max-height: unset !important;
aspect-ratio: 1 / 1;
object-fit: cover;
}
.details {
display: flex;
flex-direction: column;
gap: var(--gap-sm);
.title {
color: var(--color-contrast);
font-weight: 600;
font-size: var(--font-size-md);
}
.description {
color: var(--color-secondary);
font-size: var(--font-size-sm);
}
.stat-bar {
display: flex;
align-items: center;
gap: var(--gap-md);
margin-top: auto;
}
.stats {
display: flex;
align-items: center;
gap: var(--gap-xs);
svg {
color: var(--color-secondary);
}
}
}
}
}
.grid-table {
display: grid;
grid-template-columns:
min-content min-content minmax(min-content, 2fr)
minmax(min-content, 1fr) minmax(min-content, 1fr) min-content;
border-radius: var(--size-rounded-sm);
overflow: hidden;
margin-top: var(--spacing-card-md);
.grid-table__row {
display: contents;
> div {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
padding: var(--spacing-card-sm);
// Left edge of table
&:first-child {
padding-left: var(--spacing-card-bg);
}
// Right edge of table
&:last-child {
padding-right: var(--spacing-card-bg);
}
}
&:nth-child(2n + 1) > div {
background-color: var(--color-table-alternate-row);
}
&.grid-table__header > div {
background-color: var(--color-bg);
font-weight: bold;
color: var(--color-text-dark);
padding-top: var(--spacing-card-bg);
padding-bottom: var(--spacing-card-bg);
}
}
}
.hover-link:hover {
text-decoration: underline;
}
</style>

View File

@@ -78,7 +78,7 @@
import { TransferIcon, HistoryIcon, PayPalIcon, SaveIcon, XIcon } from 'omorphia'
const auth = await useAuth()
const minWithdraw = ref(0.1)
const minWithdraw = ref(0.01)
async function updateVenmo() {
startLoading()

View File

@@ -85,7 +85,6 @@ async function cancelPayout(id) {
await refresh()
await useAuth(auth.value.token)
} catch (err) {
console.log(err)
data.$notify({
group: 'main',
title: 'An error occurred',

View File

@@ -194,7 +194,6 @@ const selectedMethod = computed(() =>
const parsedAmount = computed(() => {
const regex = /^\$?(\d*(\.\d{2})?)$/gm
const matches = regex.exec(amount.value)
console.log(matches)
return matches && matches[1] ? parseFloat(matches[1]) : 0.0
})
const fees = computed(() => {
@@ -251,7 +250,6 @@ const agreedTerms = ref(false)
watch(country, async () => {
await refreshPayoutMethods()
console.log(payoutMethods.value)
if (payoutMethods.value && payoutMethods.value[0]) {
selectedMethodId.value = payoutMethods.value[0].id
}
@@ -285,7 +283,6 @@ async function withdraw() {
type: 'success',
})
} catch (err) {
console.log(err)
data.$notify({
group: 'main',
title: 'An error occurred',