Add transfer history and unify back elements with breadcrumbs (#1088)
* Add transfer history and unify back elements with breadcrumbs * Increase padding of breadcrumbs, include previous query params, more consistent link underlining * Prettier * Add project type text and link to project pages * Remove console.log
This commit is contained in:
parent
257b35e4ae
commit
fd28da2a3b
6
assets/images/utils/history.svg
Normal file
6
assets/images/utils/history.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<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">
|
||||
<path d="M3 3v5h5"></path>
|
||||
<path d="M3.05 13A9 9 0 1 0 6 5.3L3 8"></path>
|
||||
<path d="M12 7v5l4 2"></path>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 299 B |
@ -605,6 +605,7 @@
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
color: var(--color-link-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@ -1310,6 +1311,7 @@ textarea.known-error {
|
||||
.goto-link:hover,
|
||||
.goto-link:focus-visible {
|
||||
color: var(--color-link-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.goto-link:active {
|
||||
|
||||
@ -21,6 +21,11 @@
|
||||
<!-- Team members -->
|
||||
<template v-else-if="type === 'accepted'"><CheckIcon /> Accepted</template>
|
||||
<template v-else-if="type === 'pending'"> <ProcessingIcon /> Pending </template>
|
||||
|
||||
<!-- Transaction statuses -->
|
||||
<template v-else-if="type === 'success'"><CheckIcon /> Success</template>
|
||||
|
||||
<!-- Other -->
|
||||
<template v-else> <span class="circle" /> {{ $capitalizeString(type) }} </template>
|
||||
</span>
|
||||
</template>
|
||||
@ -105,6 +110,7 @@ export default {
|
||||
|
||||
&.type--accepted,
|
||||
&.type--admin,
|
||||
&.type--success,
|
||||
&.green {
|
||||
--badge-color: var(--color-special-green);
|
||||
}
|
||||
|
||||
54
components/ui/Breadcrumbs.vue
Normal file
54
components/ui/Breadcrumbs.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<nav class="breadcrumbs">
|
||||
<template v-for="(link, index) in linkStack" :key="index">
|
||||
<NuxtLink
|
||||
:to="link.href"
|
||||
class="breadcrumb goto-link"
|
||||
:class="{ trim: link.allowTrimming ? link.allowTrimming : false }"
|
||||
>
|
||||
{{ link.label }}
|
||||
</NuxtLink>
|
||||
<ChevronRightIcon />
|
||||
</template>
|
||||
<span class="breadcrumb">{{ currentTitle }}</span>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg'
|
||||
|
||||
defineProps({
|
||||
linkStack: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
currentTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.breadcrumbs {
|
||||
//padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
display: flex;
|
||||
margin-bottom: var(--spacing-card-bg);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
a.breadcrumb {
|
||||
padding-block: var(--spacing-card-xs);
|
||||
|
||||
&.trim {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -2,6 +2,17 @@
|
||||
<div v-if="$route.name.startsWith('type-id-settings')" class="normal-page">
|
||||
<div class="normal-page__sidebar">
|
||||
<aside class="universal-card">
|
||||
<Breadcrumbs
|
||||
current-title="Settings"
|
||||
:link-stack="[
|
||||
{ href: `/dashboard/projects`, label: 'Projects' },
|
||||
{
|
||||
href: `/${project.project_type}/${project.slug ? project.slug : project.id}`,
|
||||
label: project.title,
|
||||
allowTrimming: true,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<div class="settings-header">
|
||||
<Avatar
|
||||
:src="project.icon_url"
|
||||
@ -18,64 +29,67 @@
|
||||
</div>
|
||||
<h2>Project settings</h2>
|
||||
<NavStack>
|
||||
<NavStackItem :link="`/${project.project_type}/${project.slug}/settings`" label="General">
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
|
||||
label="General"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug}/settings/tags`"
|
||||
:link="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/settings/tags`"
|
||||
label="Tags"
|
||||
>
|
||||
<CategoriesIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug}/settings/description`"
|
||||
:link="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/settings/description`"
|
||||
label="Description"
|
||||
>
|
||||
<DescriptionIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug}/settings/license`"
|
||||
:link="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/settings/license`"
|
||||
label="License"
|
||||
>
|
||||
<LicenseIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug}/settings/links`"
|
||||
:link="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/settings/links`"
|
||||
label="Links"
|
||||
>
|
||||
<LinksIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug}/settings/members`"
|
||||
:link="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/settings/members`"
|
||||
label="Members"
|
||||
>
|
||||
<UsersIcon />
|
||||
</NavStackItem>
|
||||
<h3>Relevant pages</h3>
|
||||
<h3>Upload</h3>
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug}`"
|
||||
label="View project"
|
||||
chevron
|
||||
>
|
||||
<EyeIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug}/gallery`"
|
||||
:link="`/${project.project_type}/${project.slug ? project.slug : project.id}/gallery`"
|
||||
label="Gallery"
|
||||
chevron
|
||||
>
|
||||
<GalleryIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug}/versions`"
|
||||
:link="`/${project.project_type}/${project.slug ? project.slug : project.id}/versions`"
|
||||
label="Versions"
|
||||
chevron
|
||||
>
|
||||
<VersionIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem link="/dashboard/projects" label="All projects" chevron>
|
||||
<SettingsIcon />
|
||||
</NavStackItem>
|
||||
</NavStack>
|
||||
</aside>
|
||||
</div>
|
||||
@ -192,7 +206,17 @@
|
||||
<h1 class="title">
|
||||
{{ project.title }}
|
||||
</h1>
|
||||
<Badge v-if="$auth.user && currentMember" :type="project.status" class="status-badge" />
|
||||
<nuxt-link
|
||||
class="title-link project-type"
|
||||
:to="`/${$getProjectTypeForUrl(project.actualProjectType, project.loaders)}s`"
|
||||
>
|
||||
<BoxIcon />
|
||||
<span>{{
|
||||
$formatProjectType(
|
||||
$getProjectTypeForDisplay(project.actualProjectType, project.loaders)
|
||||
)
|
||||
}}</span>
|
||||
</nuxt-link>
|
||||
<p class="description">
|
||||
{{ project.description }}
|
||||
</p>
|
||||
@ -201,6 +225,11 @@
|
||||
:type="project.actualProjectType"
|
||||
class="categories"
|
||||
>
|
||||
<Badge
|
||||
v-if="$auth.user && currentMember"
|
||||
:type="project.status"
|
||||
class="status-badge"
|
||||
/>
|
||||
<EnvironmentIndicator
|
||||
:client-side="project.client_side"
|
||||
:server-side="project.server_side"
|
||||
@ -442,7 +471,7 @@
|
||||
<div class="featured-header">
|
||||
<h2 class="card-header">Featured versions</h2>
|
||||
<nuxt-link
|
||||
v-if="versions.length > 0 || currentMember"
|
||||
v-if="$route.name !== 'type-id-versions' && (versions.length > 0 || currentMember)"
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/versions#all-versions`"
|
||||
@ -645,7 +674,7 @@
|
||||
/>
|
||||
<div v-if="$auth.user && currentMember" class="input-group">
|
||||
<nuxt-link
|
||||
:to="`/${project.project_type}/${project.slug}/settings`"
|
||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
|
||||
class="iconified-button"
|
||||
>
|
||||
<SettingsIcon /> Settings
|
||||
@ -685,6 +714,7 @@ import OpenCollectiveIcon from '~/assets/images/external/opencollective.svg'
|
||||
import UnknownIcon from '~/assets/images/utils/unknown-donation.svg'
|
||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg'
|
||||
import EyeIcon from '~/assets/images/utils/eye.svg'
|
||||
import BoxIcon from '~/assets/images/utils/box.svg'
|
||||
import Promotion from '~/components/ads/Promotion'
|
||||
import Badge from '~/components/ui/Badge'
|
||||
import Categories from '~/components/ui/search/Categories'
|
||||
@ -710,6 +740,7 @@ import CrossIcon from '~/assets/images/utils/x.svg'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg'
|
||||
import ModerationIcon from '~/assets/images/sidebar/admin.svg'
|
||||
import { renderString } from '~/helpers/parse'
|
||||
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
|
||||
|
||||
const data = useNuxtApp()
|
||||
const route = useRoute()
|
||||
@ -1049,8 +1080,21 @@ const collapsedChecklist = ref(false)
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
margin-top: var(--spacing-card-sm);
|
||||
.project-type {
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
|
||||
svg {
|
||||
vertical-align: top;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
|
||||
@ -62,6 +62,15 @@
|
||||
</div>
|
||||
</Modal>
|
||||
<div class="version-page__title universal-card">
|
||||
<Breadcrumbs
|
||||
:current-title="version.name"
|
||||
:link-stack="[
|
||||
{
|
||||
href: getPreviousLink(),
|
||||
label: getPreviousLabel(),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<div class="version-header">
|
||||
<template v-if="isEditing">
|
||||
<input
|
||||
@ -151,19 +160,6 @@
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
Download
|
||||
</a>
|
||||
<nuxt-link
|
||||
:to="
|
||||
$router.options.history.state.back &&
|
||||
($router.options.history.state.back.includes('changelog') ||
|
||||
$router.options.history.state.back.includes('versions'))
|
||||
? $router.options.history.state.back
|
||||
: `/${project.project_type}/${project.slug ? project.slug : project.id}/versions`
|
||||
"
|
||||
class="iconified-button"
|
||||
>
|
||||
<BackIcon aria-hidden="true" />
|
||||
Back to list
|
||||
</nuxt-link>
|
||||
<button
|
||||
v-if="$auth.user && !currentMember"
|
||||
class="iconified-button"
|
||||
@ -533,19 +529,19 @@
|
||||
:allow-empty="false"
|
||||
/>
|
||||
<template v-else>
|
||||
<VersionBadge
|
||||
<Badge
|
||||
v-if="version.version_type === 'release'"
|
||||
class="value"
|
||||
type="release"
|
||||
color="green"
|
||||
/>
|
||||
<VersionBadge
|
||||
<Badge
|
||||
v-else-if="version.version_type === 'beta'"
|
||||
class="value"
|
||||
type="beta"
|
||||
color="orange"
|
||||
/>
|
||||
<VersionBadge
|
||||
<Badge
|
||||
v-else-if="version.version_type === 'alpha'"
|
||||
class="value"
|
||||
type="alpha"
|
||||
@ -678,8 +674,9 @@ import { inferVersionInfo } from '~/helpers/infer'
|
||||
import { createDataPackVersion } from '~/helpers/package'
|
||||
import { renderHighlightedString } from '~/helpers/highlight'
|
||||
|
||||
import VersionBadge from '~/components/ui/Badge'
|
||||
import Avatar from '~/components/ui/Avatar'
|
||||
import Badge from '~/components/ui/Badge'
|
||||
import Breadcrumbs from '~/components/ui/Breadcrumbs'
|
||||
import CopyCode from '~/components/ui/CopyCode'
|
||||
import Categories from '~/components/ui/search/Categories'
|
||||
import ModalConfirm from '~/components/ui/ModalConfirm'
|
||||
@ -704,12 +701,14 @@ import BackIcon from '~/assets/images/utils/left-arrow.svg'
|
||||
import BoxIcon from '~/assets/images/utils/box.svg'
|
||||
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg'
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
Modal,
|
||||
FileInput,
|
||||
Checkbox,
|
||||
ChevronRightIcon,
|
||||
Chips,
|
||||
Categories,
|
||||
DownloadIcon,
|
||||
@ -725,8 +724,9 @@ export default defineNuxtComponent({
|
||||
TransferIcon,
|
||||
UploadIcon,
|
||||
BackIcon,
|
||||
VersionBadge,
|
||||
Avatar,
|
||||
Badge,
|
||||
Breadcrumbs,
|
||||
CopyCode,
|
||||
ModalConfirm,
|
||||
ModalReport,
|
||||
@ -961,6 +961,25 @@ export default defineNuxtComponent({
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getPreviousLink() {
|
||||
if (this.$router.options.history.state.back) {
|
||||
if (
|
||||
this.$router.options.history.state.back.includes('/changelog') ||
|
||||
this.$router.options.history.state.back.includes('/versions')
|
||||
) {
|
||||
return this.$router.options.history.state.back
|
||||
}
|
||||
}
|
||||
return `/${this.project.project_type}/${
|
||||
this.project.slug ? this.project.slug : this.project.id
|
||||
}/versions`
|
||||
},
|
||||
getPreviousLabel() {
|
||||
return this.$router.options.history.state.back &&
|
||||
this.$router.options.history.state.back.endsWith('/changelog')
|
||||
? 'Changelog'
|
||||
: 'Versions'
|
||||
},
|
||||
acceptFileFromProjectType,
|
||||
renderHighlightedString,
|
||||
async addDependency(dependencyAddMode, newDependencyId, newDependencyType, hideErrors) {
|
||||
|
||||
@ -232,7 +232,7 @@
|
||||
<nuxt-link
|
||||
tabindex="-1"
|
||||
:to="`/${$getProjectTypeForUrl(project.project_type, project.loaders)}/${
|
||||
project.slug
|
||||
project.slug ? project.slug : project.id
|
||||
}`"
|
||||
>
|
||||
<Avatar
|
||||
@ -254,7 +254,7 @@
|
||||
<nuxt-link
|
||||
class="hover-link wrap-as-needed"
|
||||
:to="`/${$getProjectTypeForUrl(project.project_type, project.loaders)}/${
|
||||
project.slug
|
||||
project.slug ? project.slug : project.id
|
||||
}`"
|
||||
>
|
||||
{{ project.title }}
|
||||
@ -278,7 +278,7 @@
|
||||
<nuxt-link
|
||||
class="square-button"
|
||||
:to="`/${$getProjectTypeForUrl(project.project_type, project.loaders)}/${
|
||||
project.slug
|
||||
project.slug ? project.slug : project.id
|
||||
}/settings`"
|
||||
>
|
||||
<SettingsIcon />
|
||||
|
||||
@ -26,6 +26,9 @@
|
||||
<TransferIcon /> Transfer to
|
||||
{{ $formatWallet(auth.user.payout_data.payout_wallet) }}
|
||||
</button>
|
||||
<NuxtLink class="iconified-button" to="/dashboard/revenue/transfers">
|
||||
<HistoryIcon /> View transfer history
|
||||
</NuxtLink>
|
||||
<NuxtLink class="iconified-button" to="/settings/monetization">
|
||||
<SettingsIcon /> Monetization settings
|
||||
</NuxtLink>
|
||||
@ -90,10 +93,11 @@
|
||||
<script>
|
||||
import TransferIcon from '~/assets/images/utils/transfer.svg'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg'
|
||||
import HistoryIcon from '~/assets/images/utils/history.svg'
|
||||
import ModalTransfer from '~/components/ui/ModalTransfer'
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { TransferIcon, SettingsIcon, ModalTransfer },
|
||||
components: { TransferIcon, SettingsIcon, HistoryIcon, ModalTransfer },
|
||||
async setup() {
|
||||
const auth = await useAuth()
|
||||
|
||||
138
pages/dashboard/revenue/transfers.vue
Normal file
138
pages/dashboard/revenue/transfers.vue
Normal file
@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="universal-card payout-history">
|
||||
<Breadcrumbs
|
||||
current-title="Transfer history"
|
||||
:link-stack="[{ href: '/dashboard/revenue', label: 'Revenue' }]"
|
||||
/>
|
||||
<h2>Transfer history</h2>
|
||||
<p>
|
||||
All of your transfers from your Modrinth balance to your PayPal or Venmo accounts will be
|
||||
listed here:
|
||||
</p>
|
||||
<div class="grid-table">
|
||||
<div class="grid-table__row grid-table__header">
|
||||
<div class="desktop">Date</div>
|
||||
<div class="desktop">Status</div>
|
||||
<div class="desktop">Amount</div>
|
||||
<div class="mobile">Transaction</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(payout, index) in payouts.payouts.filter((x) => x.status === 'success')"
|
||||
:key="`payout-${index}`"
|
||||
class="grid-table__row"
|
||||
>
|
||||
<div>{{ $dayjs(payout.created).format('MMMM D, YYYY [at] h:mm A') }}</div>
|
||||
<div><Badge :type="payout.status" /></div>
|
||||
<div class="amount">{{ $formatMoney(payout.amount) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import Badge from '~/components/ui/Badge.vue'
|
||||
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
|
||||
|
||||
useHead({
|
||||
title: 'Transfer history - Modrinth',
|
||||
})
|
||||
|
||||
const auth = await useAuth()
|
||||
const app = useNuxtApp()
|
||||
|
||||
const [raw] = await Promise.all([
|
||||
useBaseFetch(`user/${auth.value.user.id}/payouts`, app.$defaultHeaders()),
|
||||
])
|
||||
|
||||
const payouts = ref(raw)
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.grid-table {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
border-radius: var(--size-rounded-sm);
|
||||
overflow: hidden;
|
||||
margin-top: var(--spacing-card-md);
|
||||
|
||||
.grid-table__header {
|
||||
.mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-table__row {
|
||||
display: contents;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-card-sm);
|
||||
|
||||
// Left edge of table
|
||||
&:first-child,
|
||||
&.mobile {
|
||||
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);
|
||||
}
|
||||
|
||||
> div {
|
||||
padding-top: var(--spacing-card-bg);
|
||||
padding-bottom: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
&.grid-table__header > div {
|
||||
background-color: var(--color-bg);
|
||||
font-weight: bold;
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 560px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.grid-table__row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> div {
|
||||
padding: var(--spacing-card-xs) var(--spacing-card-bg);
|
||||
|
||||
&:first-child,
|
||||
&.mobile {
|
||||
padding-top: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
&:last-child,
|
||||
&.mobile {
|
||||
padding-bottom: var(--spacing-card-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grid-table__header {
|
||||
.mobile {
|
||||
display: flex;
|
||||
}
|
||||
.desktop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.amount {
|
||||
color: var(--color-heading);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -44,7 +44,7 @@
|
||||
v-for="project in row"
|
||||
:key="project.id"
|
||||
class="project button-animation"
|
||||
:to="`/${project.project_type}/${project.slug}`"
|
||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}`"
|
||||
>
|
||||
<Avatar :src="project.icon_url" :alt="project.title" size="sm" loading="lazy" />
|
||||
<div class="project-info">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user