You've already forked AstralRinth
forked from didirus/AstralRinth
Rewrite Parity (#647)
* Rewrite Parity * Update SEO, fix modals, add dashes to changelog * Edit create version title * Cache tags, SEO for search/partial noscript support, notifications fix * Deploy? * Fix vercel config * Fix it again * Finish user editing * Remove broken docker build * Switch reports to modals * Update project card * Navbar line animation in most places * Add chips * Move to navlink query params * remove autogen file * Add copy code * Fix webkit text box outlines, port report modal * Update error page * Switch to avatar component * Make keyboard nav work * Fix team member spacing * improve project ID display (#676) * Bug fixes * Update OG site title * More fixes * Design tweaks * Fix card wrapping on mobile * Darken light theme color a little * Sidebar navigation for settings, notifications, and moderation * Change follow icon from a heart to a bell * Revert "Change follow icon from a heart to a bell" This reverts commit e30b46ec5d93c57df847be88eba123c7419dd03b. * Change follows icon in settings * AaaaUUUUUUUGghghhhhhhhh * Project sidebar transparent button animations * Update file input button styling and change icon remove button text * Fix environments filter condition being inverted * Remove -> revert * Improve readability of warning banners on light mode * Fix mobile menu button colors * Clean up notifications page more * Creator dashboard and monetization work * Add processing fees declarations and acknowledgement box * Beta badges * Downgrade Nuxt Vercel Builder * Update the style of button groups to be more consistent * More button consistency * Remove desktop navbar on mobile * Update home page progress indicators * Fix page jumping (Thanks @stairman06) * Make checkbox checked style consistent with other selection indicators * More home page updates * Properly reset NavRows * Move filters menu on mobile * Stylized checkbox updated to match active styling * Filters icon * Respect prefers-reduced-motion * Add most backend payouts changes (untested) * Finish tested payouts code * Allow monetization unenrolling * No longer use brand color for active highlights on standard nav elements * More consistent button group on project page * Rounded tables * Fix some things (#716) * Team member fixes + re-add changelog/versions stuff * Remove dummy data * The great CSS refactor * Remove commented out css * Give modals the legacy label styles and update profile edit labels * Fix active chip size * Remove shadow from selected chip * Require email set for CMP * Update styles of notifications to universal-card * Equivalent exchange, trading some jank for some less bad jank * Fix all gallery buttons being missing when there is only 1 image * Update project creation modal * Make beta badge less bright * Beta badge heading styling * Update withdraw processing fees info * Remove redundant label * be * Fix inverted logic * 2% is 0.02 * Add toggle to turn off alpha modpacks banner * Why warning button? * Add more footer links (#719) * Add more footer links * Move twitter * Make items on user pages less comically large and move ad above navigation * Bump text down a little on home page * Update favicon colors * Remove task list package and change default description to use bullet points * I don't remember why I made this important but let's not * Ah, yes * this doesn't actually need to be important * Align items in input groups * Adjust some spacings and clear creation modal on opening * Versions now clickable * Add link to edit page to default description * Improve monetization information text * Make wrapped text inputs not shrink * Make chips work better * smol margin on clear mod message button * Allow non-authenticated users to access settings * Remove settings anchors * Fix versions page button style on firefox * Add advanced rendering toggle * Update slug input and icon card in project edit page * Legal sidebar * h1 at beginning of description no longer has top margin * Use universal card for legal pages * Update email addresses on legal pages * Update various page titles and descriptions for consistency * Various fixes and consolidation to API URL retrieval Prevents a bug where it's possible to generate the tags under one API, switch the API, and still have tags leftover from the old API Also finally fixes staging URL being jank * Make the theme button show regardless of login state Also remove the change theme from the user dropdown because it's very redundant with the several other ways of changing theme * Make mobile profile dropdown ordering consistent with desktop * Change the base url back * Revert "Change the base url back" This reverts commit c1da89fddb83776b39f626eab33c8dc67f8a75e4. * constantize * Tiny fixes (#722) * Box-shadow chip outlines * Show settings when signed out * mods -> projects * space * Beta badge border * Slug input overflow fix, scrollable * 🙈 it will all be okay 🙊 this is just temporary 🙉 😭😭 forgive me * Fix minor bugs * fix moderation page * More fixes * Temp fix for download button * BEGONE TABLES * Fix download button Co-authored-by: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Co-authored-by: Prospector <prospectordev@gmail.com> Co-authored-by: stairman06 <36215135+stairman06@users.noreply.github.com> Co-authored-by: triphora <emmaffle@modrinth.com>
This commit is contained in:
111
components/ui/Avatar.vue
Normal file
111
components/ui/Avatar.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<img
|
||||
v-if="src"
|
||||
ref="img"
|
||||
:class="`avatar size-${size} ${circle ? 'circle' : ''}`"
|
||||
:src="src"
|
||||
:alt="alt"
|
||||
/>
|
||||
<svg
|
||||
v-else
|
||||
:class="`avatar size-${size} ${circle ? 'circle' : ''}`"
|
||||
xml:space="preserve"
|
||||
fill-rule="evenodd"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="1.5"
|
||||
clip-rule="evenodd"
|
||||
viewBox="0 0 104 104"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path fill="none" d="M0 0h103.4v103.4H0z" />
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#9a9a9a"
|
||||
stroke-width="5"
|
||||
d="M51.7 92.5V51.7L16.4 31.3l35.3 20.4L87 31.3 51.7 11 16.4 31.3v40.8l35.3 20.4L87 72V31.3L51.7 11"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Avatar',
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'sm',
|
||||
validator(value) {
|
||||
return ['xs', 'sm', 'md', 'lg'].includes(value)
|
||||
},
|
||||
},
|
||||
circle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.$refs.img && this.$refs.img.naturalWidth) {
|
||||
const isPixelated = () => {
|
||||
if (
|
||||
this.$refs.img.naturalWidth < 96 &&
|
||||
this.$refs.img.naturalWidth > 0
|
||||
) {
|
||||
this.$refs.img.style.imageRendering = 'pixelated'
|
||||
}
|
||||
}
|
||||
|
||||
if (this.$refs.img.naturalWidth) {
|
||||
isPixelated()
|
||||
} else {
|
||||
this.$refs.img.onload = isPixelated
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.avatar {
|
||||
border-radius: var(--size-rounded-icon);
|
||||
box-shadow: var(--shadow-inset-lg), var(--shadow-card);
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
background-color: var(--color-button-bg);
|
||||
object-fit: contain;
|
||||
|
||||
&.size-xs {
|
||||
--size: 2.5rem;
|
||||
box-shadow: var(--shadow-inset), var(--shadow-card);
|
||||
border-radius: var(--size-rounded-sm);
|
||||
}
|
||||
|
||||
&.size-sm {
|
||||
--size: 3rem;
|
||||
box-shadow: var(--shadow-inset), var(--shadow-card);
|
||||
border-radius: var(--size-rounded-sm);
|
||||
}
|
||||
|
||||
&.size-md {
|
||||
--size: 6rem;
|
||||
border-radius: var(--size-rounded-lg);
|
||||
}
|
||||
|
||||
&.size-lg {
|
||||
--size: 9rem;
|
||||
border-radius: var(--size-rounded-lg);
|
||||
}
|
||||
|
||||
&.circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'VersionBadge',
|
||||
name: 'Badge',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="checkbox-outer"
|
||||
class="checkbox-outer button-within"
|
||||
:class="{ disabled }"
|
||||
role="presentation"
|
||||
@click="toggle"
|
||||
@@ -71,58 +71,14 @@ export default {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
|
||||
button {
|
||||
cursor: not-allowed;
|
||||
|
||||
&:active,
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--color-button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
user-select: none;
|
||||
padding: 0.2rem 0rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
color: var(--color-heading);
|
||||
|
||||
.checkbox.collapsing svg {
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--color-button-bg-hover);
|
||||
|
||||
&.checked {
|
||||
background-color: var(--color-brand-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--color-text-dark);
|
||||
|
||||
.checkbox.collapsing svg {
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--color-button-bg-active);
|
||||
|
||||
&.checked {
|
||||
background-color: var(--color-brand-active);
|
||||
}
|
||||
}
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,18 +88,23 @@ export default {
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
min-width: 1rem;
|
||||
min-height: 1rem;
|
||||
|
||||
padding: 0;
|
||||
margin: 0 0.5rem 0 0;
|
||||
|
||||
color: var(--color-button-text);
|
||||
background-color: var(--color-button-bg);
|
||||
border-radius: var(--size-rounded-control);
|
||||
box-shadow: var(--shadow-inset-sm), 0 0 0 0 transparent;
|
||||
|
||||
&.checked {
|
||||
background-color: var(--color-brand);
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--color-text-inverted);
|
||||
color: var(--color-brand-inverted);
|
||||
stroke-width: 0.2rem;
|
||||
height: 0.8rem;
|
||||
width: 0.8rem;
|
||||
@@ -152,6 +113,7 @@ export default {
|
||||
|
||||
&.collapsing {
|
||||
background-color: transparent !important;
|
||||
box-shadow: none;
|
||||
|
||||
svg {
|
||||
color: inherit;
|
||||
@@ -166,5 +128,10 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
95
components/ui/Chips.vue
Normal file
95
components/ui/Chips.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="chips">
|
||||
<button
|
||||
v-for="item in items"
|
||||
:key="item"
|
||||
class="iconified-button"
|
||||
:class="{ selected: selected === item }"
|
||||
@click="toggleItem(item)"
|
||||
>
|
||||
<CheckIcon v-if="selected === item" />
|
||||
<span>{{ formatLabel(item) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'Chips',
|
||||
components: {
|
||||
CheckIcon,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
items: {
|
||||
required: true,
|
||||
type: Array,
|
||||
},
|
||||
neverEmpty: {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
formatLabel: {
|
||||
default: (x) => x,
|
||||
type: Function,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
selected: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value)
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.items.length > 0 && this.neverEmpty) {
|
||||
this.selected = this.items[0]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleItem(item) {
|
||||
if (this.selected === item && !this.neverEmpty) {
|
||||
this.selected = null
|
||||
} else {
|
||||
this.selected = item
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chips {
|
||||
display: flex;
|
||||
grid-gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.iconified-button {
|
||||
text-transform: capitalize;
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 0.25rem solid #ea80ff;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
color: var(--color-button-text-active);
|
||||
background-color: var(--color-brand-highlight);
|
||||
box-shadow: inset 0 0 0 transparent, 0 0 0 2px var(--color-brand);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,157 +0,0 @@
|
||||
<template>
|
||||
<Popup :show-popup="display">
|
||||
<div class="popup-delete">
|
||||
<span class="title">{{ title }}</span>
|
||||
<span class="description">
|
||||
{{ description }}
|
||||
</span>
|
||||
<label v-if="hasToType" for="confirmation" class="confirmation-label">
|
||||
<span>
|
||||
To confirm your action, please type
|
||||
<span class="confirmation-text">{{ confirmationText }}</span>
|
||||
to continue
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
v-if="hasToType"
|
||||
id="confirmation"
|
||||
v-model="confirmation_typed"
|
||||
type="text"
|
||||
placeholder="Type the input needed to continue"
|
||||
@input="type"
|
||||
/>
|
||||
<div class="actions">
|
||||
<button class="button" @click="cancel">Cancel</button>
|
||||
<button
|
||||
class="button warn-button"
|
||||
:disabled="action_disabled"
|
||||
@click="proceed"
|
||||
>
|
||||
{{ proceedLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Popup from '~/components/ui/Popup'
|
||||
|
||||
export default {
|
||||
name: 'ConfirmPopup',
|
||||
components: {
|
||||
Popup,
|
||||
},
|
||||
props: {
|
||||
confirmationText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasToType: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'No title defined',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: 'No description defined',
|
||||
required: true,
|
||||
},
|
||||
proceedLabel: {
|
||||
type: String,
|
||||
default: 'Proceed',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
action_disabled: this.hasToType,
|
||||
confirmation_typed: '',
|
||||
display: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.display = false
|
||||
},
|
||||
proceed() {
|
||||
this.display = false
|
||||
this.$emit('proceed')
|
||||
},
|
||||
type() {
|
||||
if (this.hasToType) {
|
||||
this.action_disabled =
|
||||
this.confirmation_typed.toLowerCase() !==
|
||||
this.confirmationText.toLowerCase()
|
||||
}
|
||||
},
|
||||
show() {
|
||||
this.display = true
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.popup-delete {
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media screen and (min-width: 900px) {
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
max-width: 40vw;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.25rem;
|
||||
align-self: stretch;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
word-wrap: break-word;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.confirmation-label {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.confirmation-text {
|
||||
font-weight: bold;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 1.5rem;
|
||||
|
||||
button {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
margin: 0.75rem 1rem;
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
|
||||
.warn-button {
|
||||
transition: background-color 1s, color 1s;
|
||||
color: var(--color-brand-inverted);
|
||||
background-color: var(--color-badge-red-bg);
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--color-button-bg);
|
||||
color: var(--color-button-text-disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
74
components/ui/CopyCode.vue
Normal file
74
components/ui/CopyCode.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<button
|
||||
class="code"
|
||||
:class="{ copied }"
|
||||
title="Copy code to clipboard"
|
||||
@click="copyText"
|
||||
>
|
||||
{{ text }}
|
||||
<CheckIcon v-if="copied" />
|
||||
<ClipboardCopyIcon v-else />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
import ClipboardCopyIcon from '~/assets/images/utils/clipboard-copy.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'CopyCode',
|
||||
components: {
|
||||
CheckIcon,
|
||||
ClipboardCopyIcon,
|
||||
},
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
copied: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async copyText() {
|
||||
await navigator.clipboard.writeText(this.text)
|
||||
this.copied = true
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.code {
|
||||
color: var(--color-text);
|
||||
display: flex;
|
||||
grid-gap: 0.5rem;
|
||||
font-family: var(--mono-font);
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: var(--color-code-bg);
|
||||
width: min-content;
|
||||
border-radius: 10px;
|
||||
user-select: text;
|
||||
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out,
|
||||
transform 0.05s ease-in-out, outline 0.2s ease-in-out;
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: brightness(0.85);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +1,17 @@
|
||||
<template>
|
||||
<div class="columns">
|
||||
<label class="button" @drop.prevent="addFile" @dragover.prevent>
|
||||
<span>
|
||||
<UploadIcon v-if="showIcon" />
|
||||
{{ prompt }}
|
||||
</span>
|
||||
<label
|
||||
class="iconified-button"
|
||||
@drop.prevent="handleDrop"
|
||||
@dragover.prevent
|
||||
>
|
||||
<UploadIcon v-if="showIcon" />
|
||||
{{ prompt }}
|
||||
<input
|
||||
type="file"
|
||||
:multiple="multiple"
|
||||
:accept="accept"
|
||||
@change="onChange"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
@@ -20,7 +22,7 @@ import { fileIsValid } from '~/plugins/fileUtils'
|
||||
import UploadIcon from '~/assets/images/utils/upload.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'SmartFileInput',
|
||||
name: 'FileInput',
|
||||
components: {
|
||||
UploadIcon,
|
||||
},
|
||||
@@ -48,6 +50,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
shouldAlwaysReset: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -55,8 +61,8 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange(files, shouldNotReset) {
|
||||
if (!shouldNotReset) this.files = files.target.files
|
||||
addFiles(files, shouldNotReset) {
|
||||
if (!shouldNotReset || this.shouldAlwaysReset) this.files = files
|
||||
|
||||
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true }
|
||||
this.files = [...this.files].filter((file) =>
|
||||
@@ -67,19 +73,11 @@ export default {
|
||||
this.$emit('change', this.files)
|
||||
}
|
||||
},
|
||||
addFile(e) {
|
||||
const droppedFiles = e.dataTransfer.files
|
||||
|
||||
if (!this.multiple) this.files = []
|
||||
|
||||
if (!droppedFiles) return
|
||||
;[...droppedFiles].forEach((f) => {
|
||||
this.files.push(f)
|
||||
})
|
||||
|
||||
if (!this.multiple && this.files.length > 0) this.files = [this.files[0]]
|
||||
|
||||
if (this.files.length > 0) this.onChange(null, true)
|
||||
handleDrop(e) {
|
||||
this.addFiles(e.dataTransfer.files)
|
||||
},
|
||||
handleChange(e) {
|
||||
this.addFiles(e.target.files)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -87,26 +85,12 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
label {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: var(--spacing-card-sm) var(--spacing-card-md);
|
||||
}
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 0.5rem;
|
||||
border: 2px dashed var(--color-divider-dark);
|
||||
border-radius: var(--size-rounded-control);
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
flex-direction: unset;
|
||||
margin-bottom: 0;
|
||||
max-height: unset;
|
||||
|
||||
svg {
|
||||
height: 1.25rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
121
components/ui/Modal.vue
Normal file
121
components/ui/Modal.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
:class="{
|
||||
shown: shown,
|
||||
noblur: !$orElse($store.state.cosmetics.advancedRendering, true),
|
||||
}"
|
||||
class="modal-overlay"
|
||||
@click="hide"
|
||||
/>
|
||||
<div class="modal-body" :class="{ shown: shown }">
|
||||
<div v-if="header" class="header">
|
||||
<h1>{{ header }}</h1>
|
||||
<button class="iconified-button icon-only transparent" @click="hide">
|
||||
<CrossIcon />
|
||||
</button>
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'Modal',
|
||||
components: {
|
||||
CrossIcon,
|
||||
},
|
||||
props: {
|
||||
header: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
shown: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.shown = true
|
||||
},
|
||||
hide() {
|
||||
this.shown = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.modal-overlay {
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 20;
|
||||
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
&.shown {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
background: hsla(0, 0%, 0%, 0.5);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
&.noblur {
|
||||
backdrop-filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 21;
|
||||
box-shadow: var(--shadow-raised), var(--shadow-inset);
|
||||
border-radius: var(--size-rounded-lg);
|
||||
max-height: calc(100% - 2 * var(--spacing-card-bg));
|
||||
overflow-y: auto;
|
||||
width: 600px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--color-bg);
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
|
||||
h1 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
top: calc(100% + 400px);
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: all 0.25s ease-in-out;
|
||||
|
||||
&.shown {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
width: calc(100% - 2 * var(--spacing-card-bg));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
134
components/ui/ModalConfirm.vue
Normal file
134
components/ui/ModalConfirm.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<Modal ref="modal" :header="title">
|
||||
<div class="modal-delete">
|
||||
<div class="markdown-body" v-html="$xss($md.render(description))"></div>
|
||||
<label v-if="hasToType" for="confirmation" class="confirmation-label">
|
||||
<span>
|
||||
<strong>To verify, type</strong>
|
||||
<em class="confirmation-text">{{ confirmationText }}</em>
|
||||
<strong>below:</strong>
|
||||
</span>
|
||||
</label>
|
||||
<div class="confirmation-input">
|
||||
<input
|
||||
v-if="hasToType"
|
||||
id="confirmation"
|
||||
v-model="confirmation_typed"
|
||||
type="text"
|
||||
placeholder="Type here..."
|
||||
@input="type"
|
||||
/>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button class="iconified-button" @click="cancel">
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button danger-button"
|
||||
:disabled="action_disabled"
|
||||
@click="proceed"
|
||||
>
|
||||
<TrashIcon />
|
||||
{{ proceedLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import Modal from '~/components/ui/Modal'
|
||||
|
||||
export default {
|
||||
name: 'ModalConfirm',
|
||||
components: {
|
||||
CrossIcon,
|
||||
TrashIcon,
|
||||
Modal,
|
||||
},
|
||||
props: {
|
||||
confirmationText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasToType: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'No title defined',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: 'No description defined',
|
||||
required: true,
|
||||
},
|
||||
proceedLabel: {
|
||||
type: String,
|
||||
default: 'Proceed',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
action_disabled: this.hasToType,
|
||||
confirmation_typed: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$refs.modal.hide()
|
||||
},
|
||||
proceed() {
|
||||
this.$refs.modal.hide()
|
||||
this.$emit('proceed')
|
||||
},
|
||||
type() {
|
||||
if (this.hasToType) {
|
||||
this.action_disabled =
|
||||
this.confirmation_typed.toLowerCase() !==
|
||||
this.confirmationText.toLowerCase()
|
||||
}
|
||||
},
|
||||
show() {
|
||||
this.$refs.modal.show()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-delete {
|
||||
padding: var(--spacing-card-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.markdown-body {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.confirmation-label {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.confirmation-text {
|
||||
padding-right: 0.25ch;
|
||||
}
|
||||
|
||||
.confirmation-input {
|
||||
input {
|
||||
width: 20rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-left: auto;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
240
components/ui/ModalCreation.vue
Normal file
240
components/ui/ModalCreation.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<Modal ref="modal" header="Create a project">
|
||||
<div class="modal-creation universal-labels">
|
||||
<div class="markdown-body">
|
||||
<p>
|
||||
New projects are created as drafts and can be found under your profile
|
||||
page.
|
||||
</p>
|
||||
</div>
|
||||
<label for="project-type">
|
||||
<span class="label__title"
|
||||
>Project type<span class="required">*</span></span
|
||||
>
|
||||
</label>
|
||||
<Chips
|
||||
id="project-type"
|
||||
v-model="projectType"
|
||||
:items="$tag.projectTypes.map((x) => x.display)"
|
||||
/>
|
||||
<label for="name">
|
||||
<span class="label__title">Name<span class="required">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
v-model="name"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
placeholder="Enter project name..."
|
||||
autocomplete="off"
|
||||
@input="updatedName()"
|
||||
/>
|
||||
<label for="slug">
|
||||
<span class="label__title">URL<span class="required">*</span></span>
|
||||
</label>
|
||||
<div class="text-input-wrapper">
|
||||
<div class="text-input-wrapper__before">
|
||||
https://modrinth.com/{{
|
||||
getProjectType() ? getProjectType().id : '???'
|
||||
}}/
|
||||
</div>
|
||||
<input
|
||||
id="slug"
|
||||
v-model="slug"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
@input="manualSlug = true"
|
||||
/>
|
||||
</div>
|
||||
<label for="additional-information">
|
||||
<span class="label__title">Summary<span class="required">*</span></span>
|
||||
<span class="label__description"
|
||||
>This appears in search and on the sidebar of your project's
|
||||
page.</span
|
||||
>
|
||||
</label>
|
||||
<div class="textarea-wrapper">
|
||||
<textarea
|
||||
id="additional-information"
|
||||
v-model="description"
|
||||
maxlength="256"
|
||||
/>
|
||||
</div>
|
||||
<div class="push-right input-group">
|
||||
<button class="iconified-button" @click="cancel">
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button class="iconified-button brand-button" @click="createProject">
|
||||
<CheckIcon />
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import CheckIcon from '~/assets/images/utils/right-arrow.svg?inline'
|
||||
import Modal from '~/components/ui/Modal'
|
||||
import Chips from '~/components/ui/Chips'
|
||||
|
||||
export default {
|
||||
name: 'ModalCreation',
|
||||
components: {
|
||||
Chips,
|
||||
CrossIcon,
|
||||
CheckIcon,
|
||||
Modal,
|
||||
},
|
||||
props: {
|
||||
itemType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
itemId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
projectType: this.$tag.projectTypes[0].display,
|
||||
name: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
manualSlug: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$refs.modal.hide()
|
||||
},
|
||||
getProjectType() {
|
||||
return this.$tag.projectTypes.find((x) => this.projectType === x.display)
|
||||
},
|
||||
async createProject() {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
const projectType = this.getProjectType()
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append(
|
||||
'data',
|
||||
JSON.stringify({
|
||||
title: this.name,
|
||||
project_type: projectType.actual,
|
||||
slug: this.slug,
|
||||
description: this.description,
|
||||
body: `# Placeholder description
|
||||
This is your new ${projectType.display}, ${
|
||||
this.name
|
||||
}. A checklist below is provided to help prepare for release.
|
||||
|
||||
### Before submitting for review
|
||||
- Upload at least one version
|
||||
- [Edit project description](https://modrinth.com/${this.getProjectType().id}/${
|
||||
this.slug
|
||||
}/edit)
|
||||
- Update metadata
|
||||
- Select license
|
||||
- Set up environments
|
||||
- Choose categories
|
||||
- Add source, wiki, Discord and donation links (optional)
|
||||
- Add images to gallery (optional)
|
||||
- Invite project team members (optional)
|
||||
|
||||
> Submissions are normally reviewed within 24 hours, but may take up to 48 hours
|
||||
|
||||
Questions? [Join the Modrinth Discord for support!](https://discord.gg/EUHuJHt)`,
|
||||
initial_versions: [],
|
||||
team_members: [
|
||||
{
|
||||
user_id: this.$auth.user.id,
|
||||
name: this.$auth.user.username,
|
||||
role: 'Owner',
|
||||
},
|
||||
],
|
||||
categories: [],
|
||||
client_side: 'unknown',
|
||||
server_side: 'unknown',
|
||||
license_id: this.$tag.licenses.map((it) => it.short).includes('arr')
|
||||
? 'arr'
|
||||
: this.$tag.licenses[0].short,
|
||||
is_draft: true,
|
||||
})
|
||||
)
|
||||
|
||||
console.log(formData)
|
||||
|
||||
try {
|
||||
await this.$axios({
|
||||
url: 'project',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: this.$auth.token,
|
||||
},
|
||||
})
|
||||
|
||||
this.$refs.modal.hide()
|
||||
await this.$router.replace(`/${projectType.display}/${this.slug}`)
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
show() {
|
||||
this.projectType = this.$tag.projectTypes[0].display
|
||||
this.name = ''
|
||||
this.slug = ''
|
||||
this.description = ''
|
||||
this.manualSlug = false
|
||||
this.$refs.modal.show()
|
||||
},
|
||||
updatedName() {
|
||||
if (!this.manualSlug) {
|
||||
this.slug = this.name.toLowerCase().replaceAll(' ', '-')
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-creation {
|
||||
padding: var(--spacing-card-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.markdown-body {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 20rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.text-input-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 5rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-top: var(--spacing-card-md);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
179
components/ui/ModalReport.vue
Normal file
179
components/ui/ModalReport.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<Modal ref="modal" :header="`Report ${itemType}`">
|
||||
<div class="modal-report legacy-label-styles">
|
||||
<div class="markdown-body">
|
||||
<p>
|
||||
Modding should be safe for everyone, so we take abuse and malicious
|
||||
intent seriously at Modrinth. We want to hear about harmful content on
|
||||
the site that violates our
|
||||
<nuxt-link to="/legal/terms">ToS</nuxt-link> and
|
||||
<nuxt-link to="/legal/rules">Rules</nuxt-link>. Rest assured, we’ll
|
||||
keep your identifying information private.
|
||||
</p>
|
||||
</div>
|
||||
<label class="report-label" for="report-type">
|
||||
<span>
|
||||
<strong>Reason</strong>
|
||||
</span>
|
||||
</label>
|
||||
<multiselect
|
||||
id="report-type"
|
||||
v-model="reportType"
|
||||
:options="$store.state.tag.reportTypes"
|
||||
:custom-label="
|
||||
(value) => value.charAt(0).toUpperCase() + value.slice(1)
|
||||
"
|
||||
:multiple="false"
|
||||
:searchable="false"
|
||||
:show-no-results="false"
|
||||
:show-labels="false"
|
||||
placeholder="Choose report type"
|
||||
/>
|
||||
<label class="report-label" for="additional-information">
|
||||
<strong>Additional information</strong>
|
||||
<span>
|
||||
Include links and images if possible. Markdown formatting is
|
||||
supported.
|
||||
</span>
|
||||
</label>
|
||||
<div class="textarea-wrapper">
|
||||
<Chips
|
||||
v-model="bodyViewType"
|
||||
class="separator"
|
||||
:items="['source', 'preview']"
|
||||
/>
|
||||
<div v-if="bodyViewType === 'source'" class="textarea-wrapper">
|
||||
<textarea id="body" v-model="body" spellcheck="true" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
v-highlightjs
|
||||
class="preview"
|
||||
v-html="$xss($md.render(body))"
|
||||
></div>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button class="iconified-button" @click="cancel">
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button class="iconified-button brand-button" @click="submitReport">
|
||||
<CheckIcon />
|
||||
Report
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
import Modal from '~/components/ui/Modal'
|
||||
import Chips from '~/components/ui/Chips'
|
||||
|
||||
export default {
|
||||
name: 'ModalReport',
|
||||
components: {
|
||||
Chips,
|
||||
CrossIcon,
|
||||
CheckIcon,
|
||||
Modal,
|
||||
Multiselect,
|
||||
},
|
||||
props: {
|
||||
itemType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
itemId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
reportType: '',
|
||||
body: '',
|
||||
bodyViewType: 'source',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.reportType = ''
|
||||
this.body = ''
|
||||
this.bodyViewType = 'source'
|
||||
|
||||
this.$refs.modal.hide()
|
||||
},
|
||||
async submitReport() {
|
||||
this.$nuxt.$loading.start()
|
||||
try {
|
||||
const data = {
|
||||
report_type: this.reportType,
|
||||
item_id: this.itemId,
|
||||
item_type: this.itemType,
|
||||
body: this.body,
|
||||
}
|
||||
await this.$axios.post('report', data, this.$defaultHeaders())
|
||||
|
||||
this.$refs.modal.hide()
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
show() {
|
||||
this.$refs.modal.show()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-report {
|
||||
padding: var(--spacing-card-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.markdown-body {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
max-width: 20rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.report-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-left: auto;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
margin-top: 1rem;
|
||||
height: 12rem;
|
||||
|
||||
textarea {
|
||||
// here due to a bug in safari
|
||||
max-height: 9rem;
|
||||
}
|
||||
|
||||
.preview {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
224
components/ui/ModalTransfer.vue
Normal file
224
components/ui/ModalTransfer.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<Modal ref="modal" :header="'Transfer to ' + $formatWallet(wallet)">
|
||||
<div class="modal-transfer">
|
||||
<span
|
||||
>You are initiating a transfer of your revenue from Modrinth's Creator
|
||||
Monetization Program. How much of your
|
||||
<strong>${{ balance }}</strong> balance would you like to
|
||||
transfer?</span
|
||||
>
|
||||
<div class="confirmation-input">
|
||||
<input
|
||||
id="confirmation"
|
||||
v-model="amount"
|
||||
type="text"
|
||||
pattern="^\d*(\.\d{0,2})?$"
|
||||
autocomplete="off"
|
||||
placeholder="Amount to transfer..."
|
||||
/>
|
||||
</div>
|
||||
<div class="confirm-text">
|
||||
<Checkbox
|
||||
v-if="
|
||||
isValidInput() &&
|
||||
parseInput() >= minWithdraw &&
|
||||
parseInput() <= balance
|
||||
"
|
||||
v-model="consentedFee"
|
||||
>
|
||||
<template v-if="wallet === 'venmo'"
|
||||
>I acknowledge that $0.25 will be deducted from the amount I receive
|
||||
to cover {{ $formatWallet(wallet) }} processing fees.</template
|
||||
>
|
||||
<template v-else
|
||||
>I acknowledge that an estimated {{ calcProcessingFees() }} will be
|
||||
deducted from the amount I receive to cover
|
||||
{{ $formatWallet(wallet) }} processing fees and that any excess will
|
||||
be returned to my Modrinth balance.</template
|
||||
>
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
v-if="
|
||||
isValidInput() &&
|
||||
parseInput() >= minWithdraw &&
|
||||
parseInput() <= balance
|
||||
"
|
||||
v-model="consentedAccount"
|
||||
>
|
||||
I confirm that I an initiating a transfer to the following
|
||||
{{ $formatWallet(wallet) }} account: {{ account }}
|
||||
</Checkbox>
|
||||
<span
|
||||
v-else-if="validInput && parseInput() < minWithdraw"
|
||||
class="invalid"
|
||||
>
|
||||
The amount must be at least ${{ minWithdraw }}</span
|
||||
>
|
||||
<span v-else-if="validInput && parseInput() > balance" class="invalid">
|
||||
The amount must be no more than ${{ balance }}</span
|
||||
>
|
||||
<span v-else-if="amount.length > 0" class="invalid">
|
||||
{{ amount }} is not a valid amount</span
|
||||
>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<NuxtLink class="iconified-button" to="/settings/monetization">
|
||||
<SettingsIcon /> Monetization settings
|
||||
</NuxtLink>
|
||||
<button class="iconified-button" @click="cancel">
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
:disabled="!consentedFee || !consentedAccount"
|
||||
@click="proceed"
|
||||
>
|
||||
<TransferIcon />
|
||||
Transfer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import TransferIcon from '~/assets/images/utils/transfer.svg?inline'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
||||
import Modal from '~/components/ui/Modal'
|
||||
import Checkbox from '~/components/ui/Checkbox'
|
||||
|
||||
export default {
|
||||
name: 'ModalTransfer',
|
||||
components: {
|
||||
Checkbox,
|
||||
CrossIcon,
|
||||
SettingsIcon,
|
||||
TransferIcon,
|
||||
Modal,
|
||||
},
|
||||
props: {
|
||||
wallet: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
accountType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
account: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
balance: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
minWithdraw: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
consentedFee: false,
|
||||
consentedAccount: false,
|
||||
amount: '',
|
||||
validInput: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.amount = ''
|
||||
this.consentedFee = false
|
||||
this.consentedAccount = false
|
||||
this.validInput = false
|
||||
this.$refs.modal.hide()
|
||||
},
|
||||
async proceed() {
|
||||
this.$nuxt.$loading.start()
|
||||
try {
|
||||
await this.$axios.post(
|
||||
`user/${this.$auth.user.id}/payouts`,
|
||||
{
|
||||
amount: Number(this.amount.replace('$', '')),
|
||||
},
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
await this.$store.dispatch('auth/fetchUser', {
|
||||
token: this.$auth.token,
|
||||
})
|
||||
|
||||
this.$refs.modal.hide()
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
show() {
|
||||
this.$refs.modal.show()
|
||||
},
|
||||
isValidInput() {
|
||||
const regex = /^\$?(\d*(\.\d{2})?)$/gm
|
||||
this.validInput = regex.test(this.amount) && this.amount.length > 0
|
||||
return this.validInput
|
||||
},
|
||||
parseInput() {
|
||||
const regex = /^\$?(\d*(\.\d{2})?)$/gm
|
||||
const matches = regex.exec(this.amount)
|
||||
return parseFloat(matches[1])
|
||||
},
|
||||
calcProcessingFees() {
|
||||
if (this.wallet === 'venmo') {
|
||||
return 0.25
|
||||
} else {
|
||||
return Math.max(0.25, Math.min(this.parseInput() * 0.02, 20))
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-transfer {
|
||||
padding: var(--spacing-card-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-card-sm);
|
||||
|
||||
.confirmation-input {
|
||||
input {
|
||||
width: 14rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-left: auto;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--color-text-dark);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
color: var(--color-badge-red-bg);
|
||||
}
|
||||
|
||||
.confirm-text {
|
||||
margin-top: var(--spacing-card-sm);
|
||||
min-height: 6rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-card-sm);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
200
components/ui/NavRow.vue
Normal file
200
components/ui/NavRow.vue
Normal file
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<nav class="navigation" :class="{ 'use-animation': useAnimation }">
|
||||
<NuxtLink
|
||||
v-for="(link, index) in filteredLinks"
|
||||
v-show="link.shown === undefined ? true : link.shown"
|
||||
:key="index"
|
||||
ref="linkElements"
|
||||
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
|
||||
class="nav-link button-animation"
|
||||
:class="{ 'is-active': index === activeIndex }"
|
||||
>
|
||||
<span>{{ link.label }}</span>
|
||||
</NuxtLink>
|
||||
|
||||
<div
|
||||
class="nav-indicator"
|
||||
:style="`visibility: ${
|
||||
useAnimation && activeIndex !== -1 ? 'visible' : 'hidden'
|
||||
}; left: ${indicator.left}px; right: ${indicator.right}px;
|
||||
top: ${indicator.top}px; transition: left 350ms ${
|
||||
indicator.direction === 'left'
|
||||
? 'cubic-bezier(1,0,.3,1) -140ms'
|
||||
: 'cubic-bezier(.75,-0.01,.24,.99) -40ms'
|
||||
},right 350ms ${
|
||||
indicator.direction === 'right'
|
||||
? 'cubic-bezier(1,0,.3,1) -140ms'
|
||||
: 'cubic-bezier(.75,-0.01,.24,.99) -40ms'
|
||||
}, top 100ms ease-in-out`"
|
||||
/>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NavRow',
|
||||
props: {
|
||||
links: {
|
||||
default: () => [],
|
||||
type: Array,
|
||||
},
|
||||
query: {
|
||||
default: null,
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
useAnimation: false,
|
||||
oldIndex: -1,
|
||||
activeIndex: -1,
|
||||
indicator: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 22,
|
||||
direction: 'right',
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredLinks() {
|
||||
return this.links.filter((x) => (x.shown === undefined ? true : x.shown))
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'$route.path': {
|
||||
handler() {
|
||||
this.pickLink()
|
||||
},
|
||||
},
|
||||
'$route.query': {
|
||||
handler() {
|
||||
if (this.query) this.pickLink()
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.pickLink()
|
||||
},
|
||||
methods: {
|
||||
pickLink() {
|
||||
if (this.oldIndex === -1) {
|
||||
this.useAnimation = false
|
||||
|
||||
setTimeout(() => {
|
||||
this.useAnimation = true
|
||||
}, 300)
|
||||
}
|
||||
|
||||
this.activeIndex = this.query
|
||||
? this.filteredLinks.findIndex(
|
||||
(x) =>
|
||||
(x.href === '' ? undefined : x.href) ===
|
||||
this.$route.query[this.query]
|
||||
)
|
||||
: this.filteredLinks.findIndex(
|
||||
(x) => x.href === decodeURIComponent(this.$route.path)
|
||||
)
|
||||
|
||||
if (this.activeIndex !== -1) {
|
||||
this.startAnimation()
|
||||
} else {
|
||||
this.oldIndex = -1
|
||||
}
|
||||
},
|
||||
startAnimation() {
|
||||
if (this.$refs.linkElements[this.activeIndex]) {
|
||||
this.indicator.direction =
|
||||
this.activeIndex < this.oldIndex ? 'left' : 'right'
|
||||
|
||||
this.indicator.left =
|
||||
this.$refs.linkElements[this.activeIndex].$el.offsetLeft
|
||||
this.indicator.right =
|
||||
this.$refs.linkElements[this.activeIndex].$el.parentElement
|
||||
.offsetWidth -
|
||||
this.$refs.linkElements[this.activeIndex].$el.offsetLeft -
|
||||
this.$refs.linkElements[this.activeIndex].$el.offsetWidth
|
||||
this.indicator.top =
|
||||
this.$refs.linkElements[this.activeIndex].$el.offsetTop +
|
||||
this.$refs.linkElements[this.activeIndex].$el.offsetHeight +
|
||||
1
|
||||
}
|
||||
|
||||
this.oldIndex = this.activeIndex
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navigation {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
grid-gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
|
||||
.nav-link {
|
||||
text-transform: capitalize;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text);
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
width: 100%;
|
||||
border-radius: var(--size-rounded-max);
|
||||
height: 0.25rem;
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
background-color: var(--color-brand);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
|
||||
&::after {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&:active::after {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--color-text);
|
||||
|
||||
&::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.use-animation {
|
||||
.nav-link {
|
||||
&.is-active::after {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-indicator {
|
||||
position: absolute;
|
||||
height: 0.25rem;
|
||||
border-radius: var(--size-rounded-max);
|
||||
background-color: var(--color-brand);
|
||||
transition-property: left, right, top;
|
||||
transition-duration: 350ms;
|
||||
visibility: hidden;
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
20
components/ui/NavStack.vue
Normal file
20
components/ui/NavStack.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<nav class="navigation">
|
||||
<slot />
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NavStack',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navigation {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-gap: var(--spacing-card-xs);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
64
components/ui/NavStackItem.vue
Normal file
64
components/ui/NavStackItem.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<NuxtLink class="nav-link button-base" :to="link">
|
||||
<div class="nav-content">
|
||||
<slot />
|
||||
<span>{{ label }}</span>
|
||||
<span v-if="beta" class="beta-badge">BETA</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NavStackItem',
|
||||
props: {
|
||||
link: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
label: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
beta: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nav-link {
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.25rem;
|
||||
box-shadow: none;
|
||||
|
||||
.nav-content {
|
||||
box-sizing: border-box;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: var(--size-rounded-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
flex-grow: 1;
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
&.nuxt-link-exact-active {
|
||||
.nav-content {
|
||||
color: var(--color-button-text-active);
|
||||
background-color: var(--color-button-bg);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.beta-badge {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,18 +1,19 @@
|
||||
<template>
|
||||
<div v-if="pages.length > 1" class="columns paginates">
|
||||
<button
|
||||
:class="{ disabled: currentPage === 1 }"
|
||||
<div v-if="count > 1" class="columns paginates">
|
||||
<a
|
||||
:class="{ disabled: page === 1 }"
|
||||
class="left-arrow paginate has-icon"
|
||||
aria-label="Previous Page"
|
||||
@click="currentPage !== 1 ? switchPage(currentPage - 1) : null"
|
||||
:href="linkFunction(page - 1)"
|
||||
@click.prevent="page !== 1 ? switchPage(page - 1) : null"
|
||||
>
|
||||
<LeftArrowIcon />
|
||||
</button>
|
||||
</a>
|
||||
<div
|
||||
v-for="(item, index) in pages"
|
||||
:key="'page-' + item + '-' + index"
|
||||
:class="{
|
||||
'page-number': currentPage !== item,
|
||||
'page-number': page !== item,
|
||||
shrink: item > 99,
|
||||
}"
|
||||
class="page-number-container"
|
||||
@@ -20,32 +21,32 @@
|
||||
<div v-if="item === '-'" class="has-icon">
|
||||
<GapIcon />
|
||||
</div>
|
||||
<button
|
||||
<a
|
||||
v-else
|
||||
:class="{
|
||||
'page-number current': currentPage === item,
|
||||
'page-number current': page === item,
|
||||
shrink: item > 99,
|
||||
}"
|
||||
@click="currentPage !== item ? switchPage(item) : null"
|
||||
:href="linkFunction(item)"
|
||||
@click.prevent="page !== item ? switchPage(item) : null"
|
||||
>
|
||||
{{ item }}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<a
|
||||
:class="{
|
||||
disabled: currentPage === pages[pages.length - 1],
|
||||
disabled: page === pages[pages.length - 1],
|
||||
}"
|
||||
class="right-arrow paginate has-icon"
|
||||
aria-label="Next Page"
|
||||
@click="
|
||||
currentPage !== pages[pages.length - 1]
|
||||
? switchPage(currentPage + 1)
|
||||
: null
|
||||
:href="linkFunction(page + 1)"
|
||||
@click.prevent="
|
||||
page !== pages[pages.length - 1] ? switchPage(page + 1) : null
|
||||
"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -62,17 +63,56 @@ export default {
|
||||
RightArrowIcon,
|
||||
},
|
||||
props: {
|
||||
currentPage: {
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
pages: {
|
||||
type: Array,
|
||||
count: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
linkFunction: {
|
||||
type: Function,
|
||||
default() {
|
||||
return []
|
||||
return () => '/'
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
pages() {
|
||||
let pages = []
|
||||
|
||||
if (this.count > 4) {
|
||||
if (this.page + 3 >= this.count) {
|
||||
pages = [
|
||||
1,
|
||||
'-',
|
||||
this.count - 4,
|
||||
this.count - 3,
|
||||
this.count - 2,
|
||||
this.count - 1,
|
||||
this.count,
|
||||
]
|
||||
} else if (this.page > 4) {
|
||||
pages = [
|
||||
1,
|
||||
'-',
|
||||
this.page - 1,
|
||||
this.page,
|
||||
this.page + 1,
|
||||
'-',
|
||||
this.count,
|
||||
]
|
||||
} else {
|
||||
pages = [1, 2, 3, 4, 5, '-', this.count]
|
||||
}
|
||||
} else {
|
||||
pages = Array.from({ length: this.count }, (_, i) => i + 1)
|
||||
}
|
||||
|
||||
return pages
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
switchPage(newPage) {
|
||||
this.$emit('switch-page', newPage)
|
||||
@@ -82,16 +122,18 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
button {
|
||||
box-shadow: var(--shadow-card);
|
||||
a {
|
||||
color: var(--color-button-text);
|
||||
box-shadow: var(--shadow-raised), var(--shadow-inset);
|
||||
|
||||
padding: 0;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 2rem;
|
||||
background: var(--color-raised-bg);
|
||||
|
||||
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out,
|
||||
transform 0.05s ease-in-out, outline 0.2s ease-in-out;
|
||||
|
||||
&.page-number.current {
|
||||
background: var(--color-brand);
|
||||
color: var(--color-brand-inverted);
|
||||
@@ -100,40 +142,35 @@ button {
|
||||
|
||||
&.paginate.disabled {
|
||||
background-color: transparent;
|
||||
cursor: default;
|
||||
color: var(--color-button-text-disabled);
|
||||
box-shadow: inset 0 0 0 1px var(--color-button-bg-disabled);
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(50%);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
background-color: var(--color-button-bg-hover);
|
||||
color: var(--color-button-text-hover);
|
||||
&:hover:not(&:disabled) {
|
||||
filter: brightness(0.85);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--color-button-bg-active);
|
||||
color: var(--color-button-text-active);
|
||||
&:active:not(&:disabled) {
|
||||
transform: scale(0.95);
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.has-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2em;
|
||||
svg {
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.page-number-container,
|
||||
button,
|
||||
a,
|
||||
.has-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 2em;
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
.paginates {
|
||||
@@ -143,16 +180,6 @@ button,
|
||||
.has-icon {
|
||||
margin: 0 0.3em;
|
||||
}
|
||||
font-size: 80%;
|
||||
@media screen and (min-width: 350px) {
|
||||
font-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.shrink {
|
||||
font-size: 0.9rem;
|
||||
height: 2.225em;
|
||||
width: 2.225em;
|
||||
}
|
||||
|
||||
.left-arrow {
|
||||
@@ -162,4 +189,17 @@ button,
|
||||
.right-arrow {
|
||||
margin-right: auto !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
.paginates {
|
||||
font-size: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 530px) {
|
||||
a {
|
||||
width: 2.5rem;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<div v-if="showPopup">
|
||||
<div class="popup-overlay" />
|
||||
<div class="popup-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Popup',
|
||||
props: {
|
||||
showPopup: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.popup-overlay {
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--color-button-bg);
|
||||
border: none;
|
||||
opacity: 0.6;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 11;
|
||||
box-shadow: 0 2px 3px 1px var(--color-button-bg);
|
||||
border-radius: var(--size-rounded-lg);
|
||||
max-height: 80%;
|
||||
overflow-y: auto;
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
</style>
|
||||
@@ -3,17 +3,8 @@
|
||||
<div class="columns">
|
||||
<div class="icon">
|
||||
<nuxt-link :to="`/${$getProjectTypeForUrl(type, categories)}/${id}`">
|
||||
<img
|
||||
:src="iconUrl || 'https://cdn.modrinth.com/placeholder.svg?inline'"
|
||||
:alt="name"
|
||||
loading="lazy"
|
||||
/>
|
||||
<Avatar :src="iconUrl" :alt="name" size="md" />
|
||||
</nuxt-link>
|
||||
<Categories
|
||||
:categories="categories"
|
||||
:type="type"
|
||||
class="left-categories"
|
||||
/>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="info">
|
||||
@@ -103,47 +94,57 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-side">
|
||||
<div v-if="downloads" class="stat">
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
<p>
|
||||
<strong>{{ $formatNumber(downloads) }}</strong> download<span
|
||||
v-if="downloads !== '1'"
|
||||
>s</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="follows" class="stat">
|
||||
<HeartIcon aria-hidden="true" />
|
||||
<p>
|
||||
<strong>{{ $formatNumber(follows) }}</strong> follower<span
|
||||
v-if="follows !== '1'"
|
||||
>s</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="status" class="status">
|
||||
<Badge
|
||||
v-if="status === 'approved'"
|
||||
color="green custom-circle"
|
||||
:type="status"
|
||||
/>
|
||||
<Badge
|
||||
v-else-if="status === 'processing' || status === 'archived'"
|
||||
color="yellow custom-circle"
|
||||
:type="status"
|
||||
/>
|
||||
<Badge
|
||||
v-else-if="status === 'rejected'"
|
||||
color="red custom-circle"
|
||||
:type="status"
|
||||
/>
|
||||
<Badge v-else color="gray custom-circle" :type="status" />
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-side">
|
||||
<div v-if="downloads" class="stat">
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
<p>
|
||||
<strong>{{ $formatNumber(downloads) }}</strong> download<span
|
||||
v-if="downloads !== '1'"
|
||||
>s</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="follows" class="stat">
|
||||
<HeartIcon aria-hidden="true" />
|
||||
<p>
|
||||
<strong>{{ $formatNumber(follows) }}</strong> follower<span
|
||||
v-if="follows !== '1'"
|
||||
>s</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div class="mobile-dates">
|
||||
<div class="date">
|
||||
<CalendarIcon aria-hidden="true" />
|
||||
Created {{ $dayjs(createdAt).fromNow() }}
|
||||
</div>
|
||||
<div class="date">
|
||||
<EditIcon aria-hidden="true" />
|
||||
Updated {{ $dayjs(updatedAt).fromNow() }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="status" class="status">
|
||||
<Badge
|
||||
v-if="status === 'approved'"
|
||||
color="green custom-circle"
|
||||
:type="status"
|
||||
/>
|
||||
<Badge
|
||||
v-else-if="status === 'processing' || status === 'archived'"
|
||||
color="yellow custom-circle"
|
||||
:type="status"
|
||||
/>
|
||||
<Badge
|
||||
v-else-if="status === 'rejected'"
|
||||
color="red custom-circle"
|
||||
:type="status"
|
||||
/>
|
||||
<Badge v-else color="gray custom-circle" :type="status" />
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -158,10 +159,12 @@ import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
|
||||
import EditIcon from '~/assets/images/utils/updated.svg?inline'
|
||||
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
|
||||
import HeartIcon from '~/assets/images/utils/heart.svg?inline'
|
||||
import Avatar from '~/components/ui/Avatar'
|
||||
|
||||
export default {
|
||||
name: 'ProjectCard',
|
||||
components: {
|
||||
Avatar,
|
||||
Categories,
|
||||
Badge,
|
||||
InfoIcon,
|
||||
@@ -263,6 +266,7 @@ export default {
|
||||
flex-direction: row;
|
||||
padding: var(--spacing-card-bg);
|
||||
width: calc(100% - 2 * var(--spacing-card-bg));
|
||||
overflow: hidden;
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
flex-direction: row;
|
||||
@@ -270,19 +274,14 @@ export default {
|
||||
}
|
||||
|
||||
.icon {
|
||||
img {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
margin: 0 var(--spacing-card-md) var(--spacing-card-md) 0;
|
||||
border-radius: var(--size-rounded-icon);
|
||||
object-fit: contain;
|
||||
}
|
||||
margin: 0 var(--spacing-card-md) var(--spacing-card-md) 0;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
@@ -301,11 +300,13 @@ export default {
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--color-text-dark);
|
||||
font-size: var(--font-size-xl);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.author {
|
||||
margin: auto 0 0 0;
|
||||
color: var(--color-text);
|
||||
line-break: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,8 +316,7 @@ export default {
|
||||
font-weight: bolder;
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
margin-top: 0.125rem;
|
||||
margin-bottom: 0.5rem;
|
||||
margin: 0.125rem 0;
|
||||
|
||||
svg {
|
||||
width: auto;
|
||||
@@ -352,94 +352,125 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-side {
|
||||
min-width: 12rem;
|
||||
text-align: right;
|
||||
.right-side {
|
||||
min-width: fit-content;
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
.stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
svg {
|
||||
width: auto;
|
||||
height: 1.25rem;
|
||||
svg {
|
||||
width: auto;
|
||||
height: 1.25rem;
|
||||
|
||||
margin-left: auto;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
}
|
||||
margin-left: auto;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
button,
|
||||
a {
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-dates {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
flex-wrap: wrap;
|
||||
|
||||
.card-content {
|
||||
flex-direction: column;
|
||||
|
||||
.info {
|
||||
.top {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dates {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-side {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
text-align: left;
|
||||
|
||||
.stat {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stat svg {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
button,
|
||||
a {
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.left-categories {
|
||||
display: none;
|
||||
}
|
||||
.buttons button,
|
||||
a {
|
||||
margin-left: unset;
|
||||
margin-right: unset;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 560px) {
|
||||
.card-content {
|
||||
flex-direction: column;
|
||||
margin-left: 0.75rem;
|
||||
.status {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info {
|
||||
.dates {
|
||||
.date {
|
||||
margin-bottom: 0.5rem;
|
||||
.mobile-dates {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem 0.5rem;
|
||||
color: var(--color-icon);
|
||||
font-size: var(--font-size-nm);
|
||||
|
||||
.date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-side {
|
||||
padding-top: var(--spacing-card-sm);
|
||||
|
||||
text-align: left;
|
||||
|
||||
.stat svg {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.buttons button,
|
||||
a {
|
||||
margin-left: unset;
|
||||
margin-right: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.left-categories {
|
||||
display: flex;
|
||||
margin: 0 0 0.75rem 0;
|
||||
width: 7rem;
|
||||
}
|
||||
|
||||
.right-categories {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
<template>
|
||||
<div class="columns">
|
||||
<label class="button" @drop.prevent="handleDrop" @dragover.prevent>
|
||||
<span>
|
||||
<UploadIcon v-if="showIcon" />
|
||||
{{ prompt }}
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
:multiple="multiple"
|
||||
:accept="accept"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fileIsValid } from '~/plugins/fileUtils'
|
||||
import UploadIcon from '~/assets/images/utils/upload.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'StatelessFileInput',
|
||||
components: {
|
||||
UploadIcon,
|
||||
},
|
||||
props: {
|
||||
prompt: {
|
||||
type: String,
|
||||
default: 'Select file',
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
/**
|
||||
* The max file size in bytes
|
||||
*/
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onChange(addedFiles) {
|
||||
this.$emit('change', addedFiles)
|
||||
},
|
||||
/**
|
||||
* @param {FileList} filesToAdd
|
||||
*/
|
||||
addFiles(filesToAdd) {
|
||||
if (!filesToAdd) return
|
||||
|
||||
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true }
|
||||
const validFiles = [...filesToAdd].filter((file) =>
|
||||
fileIsValid(file, validationOptions)
|
||||
)
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
this.onChange(this.multiple ? validFiles : [validFiles[0]])
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {DragEvent} e
|
||||
*/
|
||||
handleDrop(e) {
|
||||
this.addFiles(e.dataTransfer.files)
|
||||
},
|
||||
/**
|
||||
* @param {Event} e native file input event
|
||||
*/
|
||||
handleChange(e) {
|
||||
this.addFiles(e.target.files)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
label {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: var(--spacing-card-sm) var(--spacing-card-md);
|
||||
margin-bottom: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 0.5rem;
|
||||
border: 2px dashed var(--color-divider-dark);
|
||||
border-radius: var(--size-rounded-control);
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
|
||||
svg {
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,57 +0,0 @@
|
||||
<template>
|
||||
<div v-if="items.length !== 1" class="styled-tabs">
|
||||
<button
|
||||
v-for="item in items"
|
||||
:key="item"
|
||||
class="tab"
|
||||
:class="{ selected: selected === item }"
|
||||
@click="toggleItem(item)"
|
||||
>
|
||||
<span>{{ item }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ThisOrThat',
|
||||
props: {
|
||||
items: {
|
||||
required: true,
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: '',
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.items.length > 0) {
|
||||
this.selected = this.items[0]
|
||||
this.$emit('input', this.selected)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleItem(item) {
|
||||
this.selected = item
|
||||
this.$emit('input', item)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
text-transform: capitalize;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
button span::first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
@@ -71,7 +71,6 @@
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import Checkbox from '~/components/ui/Checkbox'
|
||||
import ClearIcon from '~/assets/images/utils/clear.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'VersionFilterControl',
|
||||
components: {
|
||||
@@ -144,16 +143,13 @@ export default {
|
||||
gap: var(--spacing-card-md);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.multiselect {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.checkbox-outer {
|
||||
min-width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.circle-button {
|
||||
display: flex;
|
||||
max-width: 2rem;
|
||||
@@ -161,18 +157,15 @@ export default {
|
||||
background-color: var(--color-button-bg);
|
||||
border-radius: var(--size-rounded-max);
|
||||
box-shadow: inset 0px -1px 1px rgba(17, 24, 39, 0.1);
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background-color: var(--color-button-bg-hover);
|
||||
color: var(--color-button-text-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--color-button-bg-active);
|
||||
color: var(--color-button-text-active);
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
|
||||
@@ -49,11 +49,11 @@ export default {
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
color: var(--color-icon);
|
||||
margin-right: 1em;
|
||||
margin-right: var(--spacing-card-md);
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
margin-right: 0.125rem;
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user