Projects overhaul for creators (#827)
* Projects page * Continue work on bulk edit * editLinks is now bulkEdit * Bulk Edit Links completed * Edit URL clear fields. * Create project button + other bulk buttons. * Pagination (w/o reactivity.) * Apply suggestions from code review Co-authored-by: triphora <emmaffle@modrinth.com> * Sorting fixed, broken page count though? * Only make editable projects selectable + remove delete button * Shorthand * Start using computed * Fix pagination * Add Pagination Switching * Final Style Changes * Cleanup * Action Affects dropdown * Switch to checkbox swizzle * Projects dashboard, the most hellish thing I have ever worked on * Rewrite project dashboard without tables * why's that there * Fix mod message icon * New project settings page * Remove extra slash * Bulk project route and improve styling of links UI * Remove beta label from Monetization * Relevant page links in project settings * Don't vertically center header rows * Improve error messages, add remove project icon button, add saving feedback, begin project checklist, fix license settings * Remove contextual link from project settings, disable WIP checklist * Fix bulk edit * Project checklist, add featured gallery image to project pages, fix random bugs * Remove old check * Remove icon border on grid mode and hide project status card when unnecessary * Fix build * Make checklist progress smaller and add collapsing * Remove uneven gap on nav cards * Improve wrapping of checklist * Replace project settings header link with status * Fix bugs + status stuff * Fix warns + compile error * Update wording * Hide environment type nag for project types without it * Make member dropdown match Co-authored-by: mineblock11 <93472213+mineblock11@users.noreply.github.com> Co-authored-by: triphora <emmaffle@modrinth.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me> Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
6
assets/images/utils/align-left.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">
|
||||
<line x1="21" y1="6" x2="3" y2="6"></line>
|
||||
<line x1="15" y1="12" x2="3" y2="12"></line>
|
||||
<line x1="17" y1="18" x2="3" y2="18"></line>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 328 B |
6
assets/images/utils/asterisk.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="M12 6v12"></path>
|
||||
<path d="M17.196 9 6.804 15"></path>
|
||||
<path d="m6.804 9 10.392 6"></path>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 295 B |
4
assets/images/utils/chevron-left.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<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">
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 238 B |
7
assets/images/utils/coins.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<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">
|
||||
<circle cx="8" cy="8" r="6"></circle>
|
||||
<path d="M18.09 10.37A6 6 0 1 1 10.34 18"></path>
|
||||
<path d="M7 6h1v4"></path>
|
||||
<path d="m16.71 13.88.7.71-2.82 2.82"></path>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 358 B |
5
assets/images/utils/copyright.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<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">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M15 9.354a4 4 0 1 0 0 5.292"></path>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 280 B |
6
assets/images/utils/lightbulb.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">
|
||||
<line x1="9" y1="18" x2="15" y2="18"></line>
|
||||
<line x1="10" y1="22" x2="14" y2="22"></line>
|
||||
<path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"></path>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 419 B |
5
assets/images/utils/link.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<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="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 350 B |
5
assets/images/utils/send.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<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">
|
||||
<line x1="22" y1="2" x2="11" y2="13"></line>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 293 B |
@ -1,5 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M7 7H7.01M7 3H12C12.5119 2.99999 13.0237 3.19525 13.4142 3.58579L20.4143 10.5858C21.1953 11.3668 21.1953 12.6332 20.4143 13.4142L13.4142 20.4142C12.6332 21.1953 11.3668 21.1953 10.5858 20.4142L3.58579 13.4142C3.19526 13.0237 3 12.5118 3 12V7C3 4.79086 4.79086 3 7 3Z"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<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="M12 2H2v10l9.29 9.29c.94.94 2.48.94 3.42 0l6.58-6.58c.94-.94.94-2.48 0-3.42L12 2Z"></path>
|
||||
<path d="M7 7h.01"></path>
|
||||
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 474 B After Width: | Height: | Size: 320 B |
6
assets/images/utils/tags.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="M9 5H2v7l6.29 6.29c.94.94 2.48.94 3.42 0l3.58-3.58c.94-.94.94-2.48 0-3.42L9 5Z"></path>
|
||||
<path d="M6 9.01V9"></path>
|
||||
<path d="m15 5 6.3 6.3a2.4 2.4 0 0 1 0 3.4L17 19"></path>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 378 B |
7
assets/images/utils/user-plus.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<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="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<line x1="19" y1="8" x2="19" y2="14"></line>
|
||||
<line x1="22" y1="11" x2="16" y2="11"></line>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 386 B |
7
assets/images/utils/user-x.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<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="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<line x1="17" y1="8" x2="22" y2="13"></line>
|
||||
<line x1="22" y1="8" x2="17" y2="13"></line>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 385 B |
@ -1,3 +1,478 @@
|
||||
/*
|
||||
Base components
|
||||
*/
|
||||
|
||||
.multiselect--above .multiselect__content-wrapper {
|
||||
border-top: none !important;
|
||||
border-top-left-radius: var(--size-rounded-card) !important;
|
||||
border-top-right-radius: var(--size-rounded-card) !important;
|
||||
}
|
||||
|
||||
.known-error .multiselect__tags {
|
||||
border-color: var(--color-special-red) !important;
|
||||
background-color: var(--color-warning-bg) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-warning-text);
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
color: var(--color-text) !important;
|
||||
outline: 2px solid transparent;
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.multiselect__tags {
|
||||
border-radius: var(--size-rounded-sm);
|
||||
background: var(--color-dropdown-bg);
|
||||
box-shadow: var(--shadow-inset-sm);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding-left: 7px;
|
||||
padding-top: 10px;
|
||||
|
||||
transition: background-color 0.1s ease-in-out;
|
||||
|
||||
&:active {
|
||||
background: var(--color-button-bg-hover);
|
||||
|
||||
.multiselect__spinner {
|
||||
background: var(--color-button-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__single {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.multiselect__tag {
|
||||
border-radius: var(--size-rounded-sm);
|
||||
color: var(--color-text-dark);
|
||||
background: transparent;
|
||||
border: 2px solid var(--color-brand);
|
||||
}
|
||||
|
||||
.multiselect__tag-icon {
|
||||
background: transparent;
|
||||
|
||||
&:after {
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__placeholder {
|
||||
color: var(--color-button-text);
|
||||
margin-left: 8px;
|
||||
opacity: 0.6;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__content-wrapper {
|
||||
background: var(--color-dropdown-bg);
|
||||
border: none;
|
||||
overflow-x: hidden;
|
||||
border-bottom-left-radius: var(--size-rounded-sm);
|
||||
border-bottom-right-radius: var(--size-rounded-sm);
|
||||
box-shadow: var(--shadow-inset-sm), var(--shadow-floating);
|
||||
|
||||
.multiselect__element {
|
||||
.multiselect__option--highlight {
|
||||
background: var(--color-button-bg-active);
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
|
||||
.multiselect__option--selected {
|
||||
background: var(--color-brand);
|
||||
font-weight: bold;
|
||||
color: var(--color-brand-inverted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__spinner {
|
||||
background: var(--color-dropdown-bg);
|
||||
|
||||
&:active {
|
||||
background: var(--color-button-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&.multiselect--disabled {
|
||||
background: none;
|
||||
|
||||
.multiselect__current,
|
||||
.multiselect__select {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grid-display {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-card-md);
|
||||
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
||||
|
||||
.grid-display__item {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
background-color: var(--color-bg);
|
||||
border-radius: var(--size-rounded-card);
|
||||
padding: var(--spacing-card-lg);
|
||||
gap: var(--spacing-card-md);
|
||||
|
||||
.label {
|
||||
color: var(--color-heading);
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--color-text-dark);
|
||||
font-weight: bold;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.goto-link {
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.width-12 {
|
||||
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
||||
}
|
||||
|
||||
&.width-16 {
|
||||
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Cards and body styling
|
||||
*/
|
||||
.base-card {
|
||||
@extend .padding-lg;
|
||||
|
||||
position: relative;
|
||||
min-height: var(--font-size-2xl);
|
||||
|
||||
background-color: var(--color-raised-bg);
|
||||
border-radius: var(--size-rounded-card);
|
||||
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
outline: 2px solid transparent;
|
||||
|
||||
box-shadow: var(--shadow-card);
|
||||
|
||||
.card__overlay {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
grid-gap: 0.5rem;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
border-left: 0.5rem solid var(--color-banner-side);
|
||||
padding: 1.5rem;
|
||||
line-height: 1.5;
|
||||
background-color: var(--color-banner-bg);
|
||||
color: var(--color-banner-text);
|
||||
min-height: 0;
|
||||
|
||||
a {
|
||||
/* Uses active color to increase contrast */
|
||||
color: var(--color-link-active);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.universal-labels {
|
||||
label,
|
||||
.label {
|
||||
:where(.label__title) {
|
||||
display: block;
|
||||
margin-block: var(--spacing-card-md) var(--spacing-card-sm);
|
||||
|
||||
// Same styling as h3
|
||||
color: var(--color-text-dark);
|
||||
font-size: 1.17rem;
|
||||
font-weight: bold;
|
||||
|
||||
.required {
|
||||
color: var(--color-special-red);
|
||||
}
|
||||
|
||||
&.size-card-header {
|
||||
font-size: var(--font-size-xl);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
:where(.label__description) {
|
||||
display: block;
|
||||
margin-block-end: var(--spacing-card-sm);
|
||||
|
||||
.label__subdescription {
|
||||
display: block;
|
||||
margin-block-start: var(--spacing-card-md);
|
||||
}
|
||||
}
|
||||
|
||||
:where(h1, h2, h3, h4) {
|
||||
margin-block: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.padding-lg {
|
||||
padding: var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
.padding-bg {
|
||||
padding: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
.padding-md {
|
||||
padding: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
.padding-sm {
|
||||
padding: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.padding-0 {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.padding-block-lg {
|
||||
padding-block: var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
.padding-block-bg {
|
||||
padding-block: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
.padding-block-md {
|
||||
padding-block: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
.padding-block-sm {
|
||||
padding-block: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.padding-block-0 {
|
||||
padding-block: 0;
|
||||
}
|
||||
|
||||
.padding-inline-lg {
|
||||
padding-inline: var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
.padding-inline-bg {
|
||||
padding-inline: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
.padding-inline-md {
|
||||
padding-inline: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
.padding-inline-sm {
|
||||
padding-inline: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.padding-inline-0 {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.universal-body {
|
||||
@extend .universal-labels;
|
||||
|
||||
.multiselect {
|
||||
width: 15rem;
|
||||
}
|
||||
|
||||
> :where(input + *, .input-group + *, .textarea-wrapper + *, .chips
|
||||
+ *, .resizable-textarea-wrapper + *, .input-div + *) {
|
||||
margin-block-start: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
:where(button, .button, .iconified-button) {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
input {
|
||||
width: auto;
|
||||
flex-basis: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:where(input) {
|
||||
box-sizing: border-box;
|
||||
max-height: 40px;
|
||||
width: 24rem;
|
||||
flex-basis: 24rem;
|
||||
|
||||
&:not(.stylized-toggle) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:where(.adjacent-input, &.adjacent-input) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-card-sm);
|
||||
margin-bottom: calc(var(--spacing-card-sm) + var(--spacing-card-md));
|
||||
|
||||
.iconified-button,
|
||||
.input-group {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
> :first-child {
|
||||
flex-shrink: 2;
|
||||
flex-grow: 1;
|
||||
flex-basis: min-content;
|
||||
}
|
||||
|
||||
label,
|
||||
.label {
|
||||
.label__title {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
.label__description {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.label__description:not(:first-child) {
|
||||
margin-top: var(--spacing-card-sm);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
&:not(&.small) {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
|
||||
.stylized-toggle {
|
||||
flex-basis: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
> :first-child {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
> :last-child {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
:where(.header__row) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-card-sm);
|
||||
|
||||
* {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header__title {
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
}
|
||||
}
|
||||
|
||||
> .label:first-child :where(> :first-child, .label__title),
|
||||
> label:first-child :where(> :first-child, .label__title),
|
||||
> .adjacent-input:first-child :where(> :first-child, .label__title) {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.universal-card {
|
||||
@extend .base-card;
|
||||
@extend .universal-body;
|
||||
}
|
||||
|
||||
.universal-modal {
|
||||
@extend .universal-body;
|
||||
|
||||
padding: var(--spacing-card-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
.adjacent-input,
|
||||
&.adjacent-input &:not(&.small) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: calc(600px + 2rem)) {
|
||||
.adjacent-input,
|
||||
&.adjacent-input &:not(&.small) {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-card {
|
||||
@extend .base-card;
|
||||
@extend .padding-inline-lg;
|
||||
@extend .padding-block-md;
|
||||
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 0.5rem;
|
||||
}
|
||||
|
||||
/*
|
||||
Other
|
||||
*/
|
||||
|
||||
// Here lies 𝖄𝖊 𝕸𝖆𝖗𝖌𝖎𝖓 𝕸𝖆𝖌𝖎𝖈
|
||||
// which allows to have just one wrapper div
|
||||
.iconified-input {
|
||||
@ -551,6 +1026,11 @@ tr.button-transparent {
|
||||
--text-color: var(--color-brand-inverted);
|
||||
}
|
||||
|
||||
.moderation-button {
|
||||
--background-color: var(--color-special-orange);
|
||||
--text-color: var(--color-brand-inverted);
|
||||
}
|
||||
|
||||
.brand-button {
|
||||
--background-color: var(--color-brand);
|
||||
--text-color: var(--color-brand-inverted);
|
||||
@ -840,22 +1320,23 @@ tr.button-transparent {
|
||||
}
|
||||
|
||||
.vue-notification {
|
||||
background: #44a4fc;
|
||||
border-left: 5px solid #44a4fc;
|
||||
background: var(--color-special-blue);
|
||||
border-left: 5px solid var(--color-special-blue);
|
||||
color: var(--color-brand-inverted);
|
||||
|
||||
&.success {
|
||||
background: #68cd86;
|
||||
border-left-color: #68cd86;
|
||||
background: var(--color-special-green);
|
||||
border-left-color: var(--color-special-green);
|
||||
}
|
||||
|
||||
&.warn {
|
||||
background: #ffb648;
|
||||
border-left-color: #ffb648;
|
||||
background: var(--color-special-orange);
|
||||
border-left-color: var(--color-special-orange);
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: #e54d42;
|
||||
border-left-color: #e54d42;
|
||||
background: var(--color-special-red);
|
||||
border-left-color: var(--color-special-red);
|
||||
}
|
||||
}
|
||||
|
||||
@ -977,214 +1458,6 @@ h3 {
|
||||
}
|
||||
}
|
||||
|
||||
.base-card {
|
||||
@extend .padding-lg;
|
||||
|
||||
position: relative;
|
||||
min-height: var(--font-size-2xl);
|
||||
|
||||
background-color: var(--color-raised-bg);
|
||||
border-radius: var(--size-rounded-card);
|
||||
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
outline: 2px solid transparent;
|
||||
|
||||
box-shadow: var(--shadow-card);
|
||||
|
||||
.card__overlay {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
grid-gap: 0.5rem;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
border-left: 0.5rem solid var(--color-banner-side);
|
||||
padding: 1.5rem;
|
||||
line-height: 1.5;
|
||||
background-color: var(--color-banner-bg);
|
||||
color: var(--color-banner-text);
|
||||
min-height: 0;
|
||||
|
||||
a {
|
||||
/* Uses active color to increase contrast */
|
||||
color: var(--color-link-active);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.universal-labels {
|
||||
label,
|
||||
.label {
|
||||
.label__title {
|
||||
display: block;
|
||||
margin-block: var(--spacing-card-md) var(--spacing-card-sm);
|
||||
|
||||
// Same styling as h3
|
||||
color: var(--color-text-dark);
|
||||
font-size: 1.17em;
|
||||
font-weight: bold;
|
||||
|
||||
.required {
|
||||
color: var(--color-special-red);
|
||||
}
|
||||
}
|
||||
|
||||
.label__description {
|
||||
display: block;
|
||||
margin-block-end: var(--spacing-card-sm);
|
||||
|
||||
.label__subdescription {
|
||||
display: block;
|
||||
margin-block-start: var(--spacing-card-md);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.padding-lg {
|
||||
padding: var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
.padding-bg {
|
||||
padding: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
.padding-md {
|
||||
padding: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
.padding-sm {
|
||||
padding: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.padding-block-lg {
|
||||
padding-block: var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
.padding-block-bg {
|
||||
padding-block: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
.padding-block-md {
|
||||
padding-block: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
.padding-block-sm {
|
||||
padding-block: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.padding-inline-lg {
|
||||
padding-inline: var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
.padding-inline-bg {
|
||||
padding-inline: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
.padding-inline-md {
|
||||
padding-inline: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
.padding-inline-sm {
|
||||
padding-inline: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.universal-card {
|
||||
@extend .base-card;
|
||||
@extend .universal-labels;
|
||||
|
||||
.multiselect {
|
||||
width: 15rem;
|
||||
}
|
||||
|
||||
> :where(input + *, .input-group + *) {
|
||||
margin-block-start: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
.multiselect,
|
||||
input {
|
||||
width: auto;
|
||||
flex-basis: 0;
|
||||
}
|
||||
}
|
||||
|
||||
button,
|
||||
.button,
|
||||
.iconified-button {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
input {
|
||||
box-sizing: border-box;
|
||||
max-height: 40px;
|
||||
width: 24rem;
|
||||
flex-basis: 24rem;
|
||||
|
||||
&:not(.stylized-toggle) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.adjacent-input,
|
||||
&.adjacent-input {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.iconified-button,
|
||||
.input-group {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
> :first-child {
|
||||
flex-shrink: 2;
|
||||
flex-grow: 1;
|
||||
margin-right: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
&:not(&.small) {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
margin-bottom: var(--spacing-card-sm);
|
||||
|
||||
.stylized-toggle {
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
> :first-child {
|
||||
margin-right: 0;
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
> :first-child {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
> :last-child {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.push-right {
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
@ -1206,31 +1479,6 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
.header-card {
|
||||
@extend .universal-card;
|
||||
|
||||
.header__row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-card-sm);
|
||||
|
||||
* {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header__title {
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.legacy-label-styles {
|
||||
label {
|
||||
display: flex;
|
||||
@ -1298,6 +1546,32 @@ button {
|
||||
input {
|
||||
flex-shrink: 2;
|
||||
}
|
||||
|
||||
&.shrink-first {
|
||||
:first-child {
|
||||
flex-shrink: 2;
|
||||
flex-grow: 1;
|
||||
flex-basis: min-content;
|
||||
}
|
||||
|
||||
:not(:first-child) {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> * {
|
||||
margin-bottom: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
> .multiselect {
|
||||
width: unset;
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.text-input-wrapper {
|
||||
@ -1414,6 +1688,14 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
.wrap-as-needed {
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
-webkit-hyphens: auto;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
|
||||
@ -76,7 +76,7 @@ html {
|
||||
--color-hr: var(--color-text);
|
||||
|
||||
--color-table-border: #dfe2e5;
|
||||
--color-table-alternate-row: #f6f8fa;
|
||||
--color-table-alternate-row: #f2f4f7;
|
||||
|
||||
--shadow-inset-lg: inset 0px -2px 2px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-inset: inset 0px -2px 2px hsla(221, 39%, 11%, 0.05);
|
||||
@ -204,7 +204,7 @@ html {
|
||||
--color-hr: var(--color-text);
|
||||
|
||||
--color-table-border: #4f5864;
|
||||
--color-table-alternate-row: #262a30;
|
||||
--color-table-alternate-row: #202228;
|
||||
|
||||
--shadow-inset-lg: inset 0px -2px 2px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-inset: inset 0px -2px 2px hsla(221, 39%, 11%, 0.05);
|
||||
|
||||
@ -2,14 +2,18 @@
|
||||
<img
|
||||
v-if="src"
|
||||
ref="img"
|
||||
:class="`avatar size-${size} ${circle ? 'circle' : ''}`"
|
||||
:class="`avatar size-${size} ${circle ? 'circle' : ''} ${
|
||||
noShadow ? 'no-shadow' : ''
|
||||
}`"
|
||||
:src="src"
|
||||
:alt="alt"
|
||||
:loading="loading"
|
||||
/>
|
||||
<svg
|
||||
v-else
|
||||
:class="`avatar size-${size} ${circle ? 'circle' : ''}`"
|
||||
:class="`avatar size-${size} ${circle ? 'circle' : ''} ${
|
||||
noShadow ? 'no-shadow' : ''
|
||||
}`"
|
||||
xml:space="preserve"
|
||||
fill-rule="evenodd"
|
||||
stroke-linecap="round"
|
||||
@ -52,6 +56,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
noShadow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: String,
|
||||
default: 'eager',
|
||||
@ -112,5 +120,9 @@ export default {
|
||||
&.circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&.no-shadow {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,28 +1,32 @@
|
||||
<template>
|
||||
<span :class="'version-badge ' + color + ' type--' + type">
|
||||
<template v-if="color"
|
||||
><span class="circle" /> {{ $capitalizeString(type) }}</template
|
||||
>
|
||||
<template v-else-if="type === 'admin'"
|
||||
><ModrinthIcon /> Modrinth Team</template
|
||||
>
|
||||
<template v-else-if="type === 'moderator'"
|
||||
><ModeratorIcon /> Moderator</template
|
||||
>
|
||||
<template v-if="color">
|
||||
<span class="circle" /> {{ $capitalizeString(type) }}
|
||||
</template>
|
||||
<template v-else-if="type === 'admin'">
|
||||
<ModrinthIcon /> Modrinth Team
|
||||
</template>
|
||||
<template v-else-if="type === 'moderator'">
|
||||
<ModeratorIcon /> Moderator
|
||||
</template>
|
||||
<template v-else-if="type === 'creator'"><CreatorIcon /> Creator</template>
|
||||
<template v-else-if="type === 'approved'"><ListIcon /> Listed</template>
|
||||
<template v-else-if="type === 'unlisted'"><EyeOffIcon /> Unlisted</template>
|
||||
<template v-else-if="type === 'draft'"><DraftIcon /> Draft</template>
|
||||
<template v-else-if="type === 'archived'"
|
||||
><ArchiveIcon /> Archived</template
|
||||
>
|
||||
<template v-else-if="type === 'archived'">
|
||||
<ArchiveIcon /> Archived
|
||||
</template>
|
||||
<template v-else-if="type === 'rejected'"><CrossIcon /> Rejected</template>
|
||||
<template v-else-if="type === 'processing'"
|
||||
><ProcessingIcon /> Under review</template
|
||||
>
|
||||
<template v-else
|
||||
><span class="circle" /> {{ $capitalizeString(type) }}</template
|
||||
>
|
||||
<template v-else-if="type === 'processing'">
|
||||
<ProcessingIcon /> Under review
|
||||
</template>
|
||||
<template v-else-if="type === 'accepted'"><CheckIcon /> Accepted</template>
|
||||
<template v-else-if="type === 'pending'">
|
||||
<ProcessingIcon /> Pending
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="circle" /> {{ $capitalizeString(type) }}
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@ -36,6 +40,7 @@ import DraftIcon from '~/assets/images/utils/file-text.svg?inline'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import ArchiveIcon from '~/assets/images/utils/archive.svg?inline'
|
||||
import ProcessingIcon from '~/assets/images/utils/updated.svg?inline'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'Badge',
|
||||
@ -49,6 +54,7 @@ export default {
|
||||
CrossIcon,
|
||||
ArchiveIcon,
|
||||
ProcessingIcon,
|
||||
CheckIcon,
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
@ -90,12 +96,14 @@ export default {
|
||||
--badge-color: var(--color-special-red);
|
||||
}
|
||||
|
||||
&.type--pending,
|
||||
&.type--moderator,
|
||||
&.type--processing,
|
||||
&.orange {
|
||||
--badge-color: var(--color-special-orange);
|
||||
}
|
||||
|
||||
&.type--accepted,
|
||||
&.type--admin,
|
||||
&.green {
|
||||
--badge-color: var(--color-special-green);
|
||||
|
||||
@ -73,7 +73,7 @@ export default {
|
||||
|
||||
p {
|
||||
user-select: none;
|
||||
padding: 0.2rem 0rem;
|
||||
padding: 0.2rem 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
116
components/ui/EnvironmentIndicator.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<span v-if="typeOnly" class="environment">
|
||||
<InfoIcon aria-hidden="true" />
|
||||
A {{ type }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="
|
||||
!['resourcepack', 'shader'].includes(type) &&
|
||||
!(type === 'plugin' && search) &&
|
||||
!categories.some((x) => $tag.loaderData.dataPackLoaders.includes(x))
|
||||
"
|
||||
class="environment"
|
||||
>
|
||||
<template v-if="clientSide === 'optional' && serverSide === 'optional'">
|
||||
<GlobeIcon aria-hidden="true" />
|
||||
Client or server
|
||||
</template>
|
||||
<template
|
||||
v-else-if="clientSide === 'required' && serverSide === 'required'"
|
||||
>
|
||||
<GlobeIcon aria-hidden="true" />
|
||||
Client and server
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
(clientSide === 'optional' || clientSide === 'required') &&
|
||||
(serverSide === 'optional' || serverSide === 'unsupported')
|
||||
"
|
||||
>
|
||||
<ClientIcon aria-hidden="true" />
|
||||
Client
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
(serverSide === 'optional' || serverSide === 'required') &&
|
||||
(clientSide === 'optional' || clientSide === 'unsupported')
|
||||
"
|
||||
>
|
||||
<ServerIcon aria-hidden="true" />
|
||||
Server
|
||||
</template>
|
||||
<template
|
||||
v-else-if="serverSide === 'unsupported' && clientSide === 'unsupported'"
|
||||
>
|
||||
<GlobeIcon aria-hidden="true" />
|
||||
Unsupported
|
||||
</template>
|
||||
<template v-else-if="alwaysShow">
|
||||
<InfoIcon aria-hidden="true" />
|
||||
A {{ type }}
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
import InfoIcon from '~/assets/images/utils/info.svg?inline'
|
||||
import ClientIcon from '~/assets/images/utils/client.svg?inline'
|
||||
import GlobeIcon from '~/assets/images/utils/globe.svg?inline'
|
||||
import ServerIcon from '~/assets/images/utils/server.svg?inline'
|
||||
export default {
|
||||
name: 'EnvironmentIndicator',
|
||||
components: {
|
||||
InfoIcon,
|
||||
ClientIcon,
|
||||
ServerIcon,
|
||||
GlobeIcon,
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'mod',
|
||||
},
|
||||
serverSide: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
clientSide: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
typeOnly: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
alwaysShow: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
search: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
categories: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.environment {
|
||||
display: flex;
|
||||
color: var(--color-text) !important;
|
||||
font-weight: bold;
|
||||
svg {
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -85,7 +85,6 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
label {
|
||||
flex-direction: unset;
|
||||
margin-bottom: 0;
|
||||
max-height: unset;
|
||||
|
||||
svg {
|
||||
|
||||
@ -9,15 +9,17 @@
|
||||
@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>
|
||||
<template v-if="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>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<nav class="navigation">
|
||||
<slot />
|
||||
<nav>
|
||||
<ul>
|
||||
<slot />
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
@ -11,10 +13,18 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navigation {
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-gap: var(--spacing-card-xs);
|
||||
flex-wrap: wrap;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: unset;
|
||||
text-align: unset;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,21 +1,44 @@
|
||||
<template>
|
||||
<NuxtLink class="nav-link button-base" :to="link">
|
||||
<NuxtLink v-if="link !== null" class="nav-link button-base" :to="link">
|
||||
<div class="nav-content">
|
||||
<slot />
|
||||
<span>{{ label }}</span>
|
||||
<span v-if="beta" class="beta-badge">BETA</span>
|
||||
<span v-if="chevron" class="chevron"><ChevronRightIcon /></span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<button
|
||||
v-else-if="action"
|
||||
class="nav-link button-base"
|
||||
:class="{ 'danger-button': danger }"
|
||||
@click="action"
|
||||
>
|
||||
<span class="nav-content">
|
||||
<slot />
|
||||
<span>{{ label }}</span>
|
||||
<span v-if="beta" class="beta-badge">BETA</span>
|
||||
</span>
|
||||
</button>
|
||||
<span v-else>i forgor 💀</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'NavStackItem',
|
||||
components: {
|
||||
ChevronRightIcon,
|
||||
},
|
||||
props: {
|
||||
link: {
|
||||
required: true,
|
||||
default: null,
|
||||
type: String,
|
||||
},
|
||||
action: {
|
||||
default: null,
|
||||
type: Function,
|
||||
},
|
||||
label: {
|
||||
required: true,
|
||||
type: String,
|
||||
@ -24,6 +47,14 @@ export default {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
chevron: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
danger: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -31,12 +62,20 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
.nav-link {
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text);
|
||||
background-color: transparent;
|
||||
color: var(--text-color);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.25rem;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
|
||||
:where(.nav-link) {
|
||||
--text-color: var(--color-text);
|
||||
--background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
box-sizing: border-box;
|
||||
@ -46,7 +85,7 @@ export default {
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
flex-grow: 1;
|
||||
background-color: var(--color-raised-bg);
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
&.nuxt-link-exact-active {
|
||||
@ -60,5 +99,9 @@ export default {
|
||||
.beta-badge {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
tabindex="-1"
|
||||
:to="`/${$getProjectTypeForUrl(type, categories)}/${id}`"
|
||||
>
|
||||
<Avatar :src="iconUrl" :alt="name" size="md" loading="lazy" />
|
||||
<Avatar :src="iconUrl" :alt="name" size="md" no-shadow loading="lazy" />
|
||||
</nuxt-link>
|
||||
<nuxt-link
|
||||
class="gallery"
|
||||
@ -49,59 +49,14 @@
|
||||
:type="type"
|
||||
class="tags"
|
||||
>
|
||||
<span v-if="moderation" class="environment">
|
||||
<InfoIcon aria-hidden="true" />
|
||||
A {{ projectTypeDisplay }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="
|
||||
!['resourcepack', 'shader'].includes(type) &&
|
||||
!(projectTypeDisplay === 'plugin' && search) &&
|
||||
!categories.some((x) => $tag.loaderData.dataPackLoaders.includes(x))
|
||||
"
|
||||
class="environment"
|
||||
>
|
||||
<template v-if="clientSide === 'optional' && serverSide === 'optional'">
|
||||
<GlobeIcon aria-hidden="true" />
|
||||
Client or server
|
||||
</template>
|
||||
<template
|
||||
v-else-if="clientSide === 'required' && serverSide === 'required'"
|
||||
>
|
||||
<GlobeIcon aria-hidden="true" />
|
||||
Client and server
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
(clientSide === 'optional' || clientSide === 'required') &&
|
||||
(serverSide === 'optional' || serverSide === 'unsupported')
|
||||
"
|
||||
>
|
||||
<ClientIcon aria-hidden="true" />
|
||||
Client
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
(serverSide === 'optional' || serverSide === 'required') &&
|
||||
(clientSide === 'optional' || clientSide === 'unsupported')
|
||||
"
|
||||
>
|
||||
<ServerIcon aria-hidden="true" />
|
||||
Server
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
serverSide === 'unsupported' && clientSide === 'unsupported'
|
||||
"
|
||||
>
|
||||
<GlobeIcon aria-hidden="true" />
|
||||
Unsupported
|
||||
</template>
|
||||
<template v-else-if="moderation">
|
||||
<InfoIcon aria-hidden="true" />
|
||||
A {{ projectTypeDisplay }}
|
||||
</template>
|
||||
</span>
|
||||
<EnvironmentIndicator
|
||||
:type-only="moderation"
|
||||
:client-side="clientSide"
|
||||
:server-side="serverSide"
|
||||
:type="projectTypeDisplay"
|
||||
:search="search"
|
||||
:categories="categories"
|
||||
/>
|
||||
</Categories>
|
||||
<div class="stats">
|
||||
<div v-if="downloads" class="stat">
|
||||
@ -150,11 +105,8 @@
|
||||
<script>
|
||||
import Categories from '~/components/ui/search/Categories'
|
||||
import Badge from '~/components/ui/Badge'
|
||||
import EnvironmentIndicator from '~/components/ui/EnvironmentIndicator'
|
||||
|
||||
import InfoIcon from '~/assets/images/utils/info.svg?inline'
|
||||
import ClientIcon from '~/assets/images/utils/client.svg?inline'
|
||||
import GlobeIcon from '~/assets/images/utils/globe.svg?inline'
|
||||
import ServerIcon from '~/assets/images/utils/server.svg?inline'
|
||||
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'
|
||||
@ -164,13 +116,10 @@ import Avatar from '~/components/ui/Avatar'
|
||||
export default {
|
||||
name: 'ProjectCard',
|
||||
components: {
|
||||
EnvironmentIndicator,
|
||||
Avatar,
|
||||
Categories,
|
||||
Badge,
|
||||
InfoIcon,
|
||||
ClientIcon,
|
||||
ServerIcon,
|
||||
GlobeIcon,
|
||||
CalendarIcon,
|
||||
EditIcon,
|
||||
DownloadIcon,
|
||||
@ -364,7 +313,7 @@ export default {
|
||||
img,
|
||||
svg {
|
||||
border-radius: var(--size-rounded-lg);
|
||||
border: 0.25rem solid var(--color-raised-bg);
|
||||
border: 4px solid var(--color-raised-bg);
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
@ -427,6 +376,11 @@ export default {
|
||||
|
||||
.icon {
|
||||
margin-top: calc(var(--spacing-card-bg) - var(--spacing-card-sm));
|
||||
|
||||
img,
|
||||
svg {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
|
||||
440
components/ui/ProjectPublishingChecklist.vue
Normal file
@ -0,0 +1,440 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="
|
||||
$auth.user &&
|
||||
currentMember &&
|
||||
nags.filter((x) => x.condition).length > 0 &&
|
||||
project.status === 'draft'
|
||||
"
|
||||
class="author-actions universal-card"
|
||||
>
|
||||
<div class="header__row">
|
||||
<div class="header__title">
|
||||
<h2>Publishing checklist</h2>
|
||||
<div class="checklist">
|
||||
<span class="checklist__title">Progress:</span>
|
||||
<div class="checklist__items">
|
||||
<div
|
||||
v-for="nag in nags"
|
||||
:key="`checklist-${nag.id}`"
|
||||
v-tooltip="nag.title"
|
||||
class="circle"
|
||||
:class="'circle ' + (!nag.condition ? 'done ' : '') + nag.status"
|
||||
>
|
||||
<CheckIcon v-if="!nag.condition" />
|
||||
<RequiredIcon v-else-if="nag.status === 'required'" />
|
||||
<SuggestionIcon v-else-if="nag.status === 'suggestion'" />
|
||||
<ModerationIcon v-else-if="nag.status === 'review'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<button
|
||||
class="square-button"
|
||||
:class="{ 'not-collapsed': !collapsed }"
|
||||
@click="toggleCollapsed()"
|
||||
>
|
||||
<DropdownIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!collapsed" class="grid-display width-16">
|
||||
<div
|
||||
v-for="nag in nags.filter((x) => x.condition)"
|
||||
:key="nag.id"
|
||||
class="grid-display__item"
|
||||
>
|
||||
<span class="label">
|
||||
<RequiredIcon
|
||||
v-if="nag.status === 'required'"
|
||||
v-tooltip="'Required'"
|
||||
:class="nag.status"
|
||||
/>
|
||||
<SuggestionIcon
|
||||
v-else-if="nag.status === 'suggestion'"
|
||||
v-tooltip="'Suggestion'"
|
||||
:class="nag.status"
|
||||
/>
|
||||
<ModerationIcon
|
||||
v-else-if="nag.status === 'review'"
|
||||
v-tooltip="'Review'"
|
||||
:class="nag.status"
|
||||
/>{{ nag.title }}</span
|
||||
>
|
||||
{{ nag.description }}
|
||||
<NuxtLink
|
||||
v-if="nag.link"
|
||||
:class="{ invisible: nag.link.hide }"
|
||||
class="goto-link"
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/${nag.link.path}`"
|
||||
>
|
||||
{{ nag.link.title }}
|
||||
<ChevronRightIcon
|
||||
class="featured-header-chevron"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NuxtLink>
|
||||
<button
|
||||
v-else-if="nag.action"
|
||||
class="iconified-button moderation-button"
|
||||
:disabled="nag.action.disabled()"
|
||||
@click="nag.action.onClick"
|
||||
>
|
||||
<SendIcon />
|
||||
{{ nag.action.title }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?inline'
|
||||
import DropdownIcon from '~/assets/images/utils/dropdown.svg?inline'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
import RequiredIcon from '~/assets/images/utils/asterisk.svg?inline'
|
||||
import SuggestionIcon from '~/assets/images/utils/lightbulb.svg?inline'
|
||||
import ModerationIcon from '~/assets/images/sidebar/admin.svg?inline'
|
||||
import SendIcon from '~/assets/images/utils/send.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'ProjectPublishingChecklist',
|
||||
components: {
|
||||
ChevronRightIcon,
|
||||
DropdownIcon,
|
||||
CheckIcon,
|
||||
RequiredIcon,
|
||||
SuggestionIcon,
|
||||
ModerationIcon,
|
||||
SendIcon,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isSettings: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
routeName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
setProcessing: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'setProcessing function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
toggleCollapsed: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'toggleCollapsed function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
featuredGalleryImage() {
|
||||
return this.project.gallery.find((img) => img.featured)
|
||||
},
|
||||
nags() {
|
||||
return [
|
||||
{
|
||||
condition:
|
||||
this.project.body === '' ||
|
||||
this.project.body.startsWith('# Placeholder description'),
|
||||
title: 'Add a description',
|
||||
id: 'add-description',
|
||||
description:
|
||||
"A description that clearly describes the project's purpose and function is required.",
|
||||
status: 'required',
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: 'Visit description settings',
|
||||
hide: this.routeName === 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: !this.project.icon_url,
|
||||
title: 'Add an icon',
|
||||
id: 'add-icon',
|
||||
description:
|
||||
'Your project should have a nice-looking icon to uniquely identify your project at a glance.',
|
||||
status: 'suggestion',
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: 'Visit general settings',
|
||||
hide: this.routeName === 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: !this.featuredGalleryImage,
|
||||
title: 'Feature a gallery image',
|
||||
id: 'feature-gallery-image',
|
||||
description:
|
||||
'Featured gallery images may be the first impression of many users.',
|
||||
status: 'suggestion',
|
||||
link: {
|
||||
path: 'gallery',
|
||||
title: 'Visit gallery page',
|
||||
hide: this.routeName === 'type-id-gallery',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: this.versions.length < 1,
|
||||
title: 'Upload a version',
|
||||
id: 'upload-version',
|
||||
description:
|
||||
'At least one version is required for a project to be submitted for review.',
|
||||
status: 'required',
|
||||
link: {
|
||||
path: 'versions',
|
||||
title: 'Visit versions page',
|
||||
hide: this.routeName === 'type-id-versions',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: this.project.categories.length < 1,
|
||||
title: 'Select tags',
|
||||
id: 'select-tags',
|
||||
description: 'Select all tags that apply to your project.',
|
||||
status: 'suggestion',
|
||||
link: {
|
||||
path: 'settings/tags',
|
||||
title: 'Visit tag settings',
|
||||
hide: this.routeName === 'type-id-settings-tags',
|
||||
},
|
||||
},
|
||||
{
|
||||
hide:
|
||||
this.project.project_type === 'resourcepack' ||
|
||||
this.project.project_type === 'plugin' ||
|
||||
this.project.project_type === 'shader' ||
|
||||
this.project.project_type === 'datapack',
|
||||
condition:
|
||||
this.project.client_side === 'unknown' ||
|
||||
this.project.server_side === 'unknown',
|
||||
title: 'Select supported environments',
|
||||
id: 'select-environments',
|
||||
description: `Select if the ${this.$formatProjectType(
|
||||
this.project.project_type
|
||||
).toLowerCase()} functions on the client-side and/or server-side.`,
|
||||
status: 'required',
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: 'Visit general settings',
|
||||
hide: this.routeName === 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: this.project.status === 'draft',
|
||||
title: 'Submit for review',
|
||||
id: 'submit-for-review',
|
||||
description:
|
||||
'Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.',
|
||||
status: 'review',
|
||||
link: null,
|
||||
action: {
|
||||
onClick: this.submitForReview,
|
||||
title: 'Submit for review',
|
||||
disabled: () =>
|
||||
this.nags.filter((x) => x.condition && x.status === 'required')
|
||||
.length > 0,
|
||||
},
|
||||
},
|
||||
]
|
||||
.filter((x) => !x.hide)
|
||||
.sort((a, b) =>
|
||||
this.sortByTrue(
|
||||
!a.condition,
|
||||
!b.condition,
|
||||
this.sortByTrue(
|
||||
a.status === 'required',
|
||||
b.status === 'required',
|
||||
this.sortByFalse(a.status === 'review', b.status === 'review')
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sortByTrue(a, b, ifEqual = 0) {
|
||||
if (a === b) {
|
||||
return ifEqual
|
||||
} else if (a) {
|
||||
return -1
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
},
|
||||
sortByFalse(a, b, ifEqual = 0) {
|
||||
if (a === b) {
|
||||
return ifEqual
|
||||
} else if (b) {
|
||||
return -1
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
},
|
||||
async submitForReview() {
|
||||
if (
|
||||
this.nags.filter((x) => x.condition && x.status === 'required')
|
||||
.length === 0
|
||||
) {
|
||||
await this.setProcessing()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.author-actions {
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.header__row {
|
||||
align-items: center;
|
||||
column-gap: var(--spacing-card-lg);
|
||||
row-gap: var(--spacing-card-md);
|
||||
max-width: 100%;
|
||||
|
||||
.header__title {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
column-gap: var(--spacing-card-lg);
|
||||
row-gap: var(--spacing-card-md);
|
||||
flex-basis: min-content;
|
||||
|
||||
h2 {
|
||||
margin: 0 auto 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
svg {
|
||||
transition: transform 0.25s ease-in-out;
|
||||
}
|
||||
|
||||
&.not-collapsed svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grid-display__item .label {
|
||||
display: flex;
|
||||
gap: var(--spacing-card-xs);
|
||||
align-items: center;
|
||||
|
||||
.required {
|
||||
color: var(--color-special-red);
|
||||
}
|
||||
|
||||
.suggestion {
|
||||
color: var(--color-special-purple);
|
||||
}
|
||||
|
||||
.review {
|
||||
color: var(--color-special-orange);
|
||||
}
|
||||
}
|
||||
|
||||
.checklist {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-card-xs);
|
||||
width: fit-content;
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
|
||||
.checklist__title {
|
||||
font-weight: bold;
|
||||
margin-right: var(--spacing-card-xs);
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
|
||||
.checklist__items {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-card-xs);
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.circle {
|
||||
--circle-size: 2rem;
|
||||
--background-color: var(--color-bg);
|
||||
--content-color: var(--color-special-gray);
|
||||
width: var(--circle-size);
|
||||
height: var(--circle-size);
|
||||
border-radius: 50%;
|
||||
background-color: var(--background-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
color: var(--content-color);
|
||||
width: calc(var(--circle-size) / 2);
|
||||
height: calc(var(--circle-size) / 2);
|
||||
}
|
||||
|
||||
&.required {
|
||||
--content-color: var(--color-special-red);
|
||||
}
|
||||
|
||||
&.suggestion {
|
||||
--content-color: var(--color-special-purple);
|
||||
}
|
||||
|
||||
&.review {
|
||||
--content-color: var(--color-special-orange);
|
||||
}
|
||||
|
||||
&.done {
|
||||
--background-color: var(--color-special-green);
|
||||
--content-color: var(--color-brand-inverted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,5 +1,111 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="isSettings" class="normal-page">
|
||||
<div class="normal-page__sidebar">
|
||||
<aside class="universal-card">
|
||||
<div class="settings-header">
|
||||
<Avatar
|
||||
:src="project.icon_url"
|
||||
:alt="project.title"
|
||||
size="sm"
|
||||
class="settings-header__icon"
|
||||
/>
|
||||
<div class="settings-header__text">
|
||||
<h1 class="wrap-as-needed">{{ project.title }}</h1>
|
||||
<Badge :type="project.status" />
|
||||
</div>
|
||||
</div>
|
||||
<h2>Project settings</h2>
|
||||
<NavStack>
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug}/settings`"
|
||||
label="General"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug}/settings/tags`"
|
||||
label="Tags"
|
||||
>
|
||||
<CategoriesIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug}/settings/description`"
|
||||
label="Description"
|
||||
>
|
||||
<DescriptionIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug}/settings/license`"
|
||||
label="License"
|
||||
>
|
||||
<LicenseIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug}/settings/links`"
|
||||
label="Links"
|
||||
>
|
||||
<LinksIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug}/settings/members`"
|
||||
label="Members"
|
||||
>
|
||||
<UsersIcon />
|
||||
</NavStackItem>
|
||||
<h3>Relevant pages</h3>
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug}`"
|
||||
label="View project"
|
||||
chevron
|
||||
>
|
||||
<EyeIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug}/gallery`"
|
||||
label="Gallery"
|
||||
chevron
|
||||
>
|
||||
<GalleryIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
:link="`/${project.project_type}/${project.slug}/versions`"
|
||||
label="Versions"
|
||||
chevron
|
||||
>
|
||||
<VersionIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem link="/dashboard/projects" label="All projects" chevron>
|
||||
<SettingsIcon />
|
||||
</NavStackItem>
|
||||
</NavStack>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<ProjectPublishingChecklist
|
||||
:project="project"
|
||||
:versions="versions"
|
||||
:current-member="currentMember"
|
||||
:is-settings="isSettings"
|
||||
:route-name="routeName"
|
||||
:set-processing="setProcessing"
|
||||
:collapsed="collapsedChecklist"
|
||||
:toggle-collapsed="toggleChecklistCollapse"
|
||||
/>
|
||||
<NuxtChild
|
||||
:project.sync="project"
|
||||
:versions.sync="versions"
|
||||
:featured-versions.sync="featuredVersions"
|
||||
:members.sync="members"
|
||||
:current-member="currentMember"
|
||||
:all-members.sync="allMembers"
|
||||
:dependencies.sync="dependencies"
|
||||
:patch-project="patchProject"
|
||||
:patch-icon="patchIcon"
|
||||
:update-icon="updateIcon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Modal
|
||||
ref="modal_license"
|
||||
:header="project.license.name ? project.license.name : 'License'"
|
||||
@ -21,173 +127,156 @@
|
||||
}"
|
||||
>
|
||||
<div class="normal-page__sidebar">
|
||||
<div class="header card">
|
||||
<div
|
||||
class="header project__header base-card padding-0"
|
||||
:class="{ 'has-featured-image': featuredGalleryImage }"
|
||||
>
|
||||
<nuxt-link
|
||||
class="project__gallery"
|
||||
tabindex="-1"
|
||||
:to="
|
||||
'/' +
|
||||
project.project_type +
|
||||
'/' +
|
||||
(project.slug ? project.slug : project.id)
|
||||
(project.slug ? project.slug : project.id) +
|
||||
'/gallery'
|
||||
"
|
||||
>
|
||||
<Avatar :src="project.icon_url" :alt="project.title" size="md" />
|
||||
</nuxt-link>
|
||||
<nuxt-link
|
||||
:to="
|
||||
'/' +
|
||||
project.project_type +
|
||||
'/' +
|
||||
(project.slug ? project.slug : project.id)
|
||||
"
|
||||
>
|
||||
<h1 class="title">{{ project.title }}</h1>
|
||||
<img
|
||||
v-if="featuredGalleryImage"
|
||||
:src="featuredGalleryImage.url"
|
||||
:alt="
|
||||
featuredGalleryImage.description
|
||||
? featuredGalleryImage.description
|
||||
: featuredGalleryImage.title
|
||||
"
|
||||
/>
|
||||
</nuxt-link>
|
||||
<div
|
||||
v-if="
|
||||
project.project_type !== 'resourcepack' &&
|
||||
project.project_type !== 'plugin' &&
|
||||
project.project_type !== 'shader' &&
|
||||
project.project_type !== 'datapack'
|
||||
"
|
||||
class="project__header__content universal-card full-width-inputs"
|
||||
>
|
||||
<div
|
||||
v-if="
|
||||
project.client_side === 'optional' &&
|
||||
project.server_side === 'optional'
|
||||
"
|
||||
class="side-descriptor"
|
||||
<Avatar
|
||||
:src="project.icon_url"
|
||||
:alt="project.title"
|
||||
size="md"
|
||||
class="project__icon"
|
||||
no-shadow
|
||||
/>
|
||||
<h1 class="title">
|
||||
{{ project.title }}
|
||||
</h1>
|
||||
<Badge
|
||||
v-if="$auth.user && currentMember"
|
||||
:type="project.status"
|
||||
class="status-badge"
|
||||
/>
|
||||
<p class="description">
|
||||
{{ project.description }}
|
||||
</p>
|
||||
<Categories
|
||||
:categories="project.categories"
|
||||
:type="project.actualProjectType"
|
||||
class="categories"
|
||||
>
|
||||
<InfoIcon aria-hidden="true" />
|
||||
Universal {{ projectTypeDisplay }}
|
||||
<EnvironmentIndicator
|
||||
:client-side="project.client_side"
|
||||
:server-side="project.server_side"
|
||||
:type="project.project_type"
|
||||
/>
|
||||
</Categories>
|
||||
<hr class="card-divider" />
|
||||
<div class="primary-stat">
|
||||
<DownloadIcon class="primary-stat__icon" aria-hidden="true" />
|
||||
<div class="primary-stat__text">
|
||||
<span class="primary-stat__counter">
|
||||
{{ $formatNumber(project.downloads) }}
|
||||
</span>
|
||||
download<span v-if="project.downloads !== 1">s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
(project.client_side === 'optional' ||
|
||||
project.client_side === 'required') &&
|
||||
(project.server_side === 'optional' ||
|
||||
project.server_side === 'unsupported')
|
||||
"
|
||||
class="side-descriptor"
|
||||
>
|
||||
<InfoIcon aria-hidden="true" />
|
||||
Client {{ projectTypeDisplay }}
|
||||
<div class="primary-stat">
|
||||
<HeartIcon class="primary-stat__icon" aria-hidden="true" />
|
||||
<div class="primary-stat__text">
|
||||
<span class="primary-stat__counter">
|
||||
{{ $formatNumber(project.followers) }}
|
||||
</span>
|
||||
follower<span v-if="project.followers !== 1">s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
(project.server_side === 'optional' ||
|
||||
project.server_side === 'required') &&
|
||||
(project.client_side === 'optional' ||
|
||||
project.client_side === 'unsupported')
|
||||
"
|
||||
class="side-descriptor"
|
||||
>
|
||||
<InfoIcon aria-hidden="true" />
|
||||
Server {{ projectTypeDisplay }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="description">
|
||||
{{ project.description }}
|
||||
</p>
|
||||
<Categories
|
||||
:categories="project.categories"
|
||||
:type="project.actualProjectType"
|
||||
class="categories"
|
||||
/>
|
||||
<hr class="card-divider" />
|
||||
<div class="primary-stat">
|
||||
<DownloadIcon class="primary-stat__icon" aria-hidden="true" />
|
||||
<div class="primary-stat__text">
|
||||
<span class="primary-stat__counter">
|
||||
{{ $formatNumber(project.downloads) }}
|
||||
</span>
|
||||
download<span v-if="project.downloads !== 1">s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="primary-stat">
|
||||
<HeartIcon class="primary-stat__icon" aria-hidden="true" />
|
||||
<div class="primary-stat__text">
|
||||
<span class="primary-stat__counter">
|
||||
{{ $formatNumber(project.followers) }}
|
||||
</span>
|
||||
follower<span v-if="project.followers !== 1">s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dates">
|
||||
<div
|
||||
v-tooltip="
|
||||
$dayjs(project.published).format('MMMM D, YYYY [at] h:mm:ss A')
|
||||
"
|
||||
class="date"
|
||||
>
|
||||
<CalendarIcon aria-hidden="true" />
|
||||
<span class="label">Created</span>
|
||||
<span class="value">{{
|
||||
$dayjs(project.published).fromNow()
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-tooltip="
|
||||
$dayjs(project.updated).format('MMMM D, YYYY [at] h:mm:ss A')
|
||||
"
|
||||
class="date"
|
||||
>
|
||||
<UpdateIcon aria-hidden="true" />
|
||||
<span class="label">Updated</span>
|
||||
<span class="value">{{ $dayjs(project.updated).fromNow() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
<div class="input-group">
|
||||
<template v-if="$auth.user">
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="$refs.modal_project_report.show()"
|
||||
<div class="dates">
|
||||
<div
|
||||
v-tooltip="
|
||||
$dayjs(project.published).format(
|
||||
'MMMM D, YYYY [at] h:mm:ss A'
|
||||
)
|
||||
"
|
||||
class="date"
|
||||
>
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</button>
|
||||
<button
|
||||
v-if="!$user.follows.find((x) => x.id === project.id)"
|
||||
class="iconified-button"
|
||||
@click="$store.dispatch('user/followProject', project)"
|
||||
<CalendarIcon aria-hidden="true" />
|
||||
<span class="label">Created</span>
|
||||
<span class="value">{{
|
||||
$dayjs(project.published).fromNow()
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-tooltip="
|
||||
$dayjs(project.updated).format('MMMM D, YYYY [at] h:mm:ss A')
|
||||
"
|
||||
class="date"
|
||||
>
|
||||
<HeartIcon aria-hidden="true" />
|
||||
Follow
|
||||
</button>
|
||||
<button
|
||||
v-if="$user.follows.find((x) => x.id === project.id)"
|
||||
class="iconified-button"
|
||||
@click="$store.dispatch('user/unfollowProject', project)"
|
||||
>
|
||||
<HeartIcon fill="currentColor" aria-hidden="true" />
|
||||
Unfollow
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a
|
||||
class="iconified-button"
|
||||
:href="authUrl"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</a>
|
||||
<a
|
||||
class="iconified-button"
|
||||
:href="authUrl"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
<HeartIcon aria-hidden="true" />
|
||||
Follow
|
||||
</a>
|
||||
</template>
|
||||
<UpdateIcon aria-hidden="true" />
|
||||
<span class="label">Updated</span>
|
||||
<span class="value">{{
|
||||
$dayjs(project.updated).fromNow()
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
<div class="input-group">
|
||||
<template v-if="$auth.user">
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="$refs.modal_project_report.show()"
|
||||
>
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</button>
|
||||
<button
|
||||
v-if="!$user.follows.find((x) => x.id === project.id)"
|
||||
class="iconified-button"
|
||||
@click="$store.dispatch('user/followProject', project)"
|
||||
>
|
||||
<HeartIcon aria-hidden="true" />
|
||||
Follow
|
||||
</button>
|
||||
<button
|
||||
v-if="$user.follows.find((x) => x.id === project.id)"
|
||||
class="iconified-button"
|
||||
@click="$store.dispatch('user/unfollowProject', project)"
|
||||
>
|
||||
<HeartIcon fill="currentColor" aria-hidden="true" />
|
||||
Unfollow
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a class="iconified-button" :href="authUrl">
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</a>
|
||||
<a class="iconified-button" :href="authUrl">
|
||||
<HeartIcon fill="currentColor" aria-hidden="true" />
|
||||
Follow
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
currentMember &&
|
||||
(project.status !== 'approved' ||
|
||||
((project.status !== 'approved' &&
|
||||
project.status !== 'draft' &&
|
||||
project.status !== 'processing') ||
|
||||
(project.moderator_message &&
|
||||
(project.moderator_message.message ||
|
||||
project.moderator_message.body)))
|
||||
@ -207,19 +296,6 @@
|
||||
which is below. Do not resubmit until you've addressed the message
|
||||
from the moderators!
|
||||
</p>
|
||||
<p v-if="project.status === 'processing'">
|
||||
Your project is currently not viewable by people who are not part
|
||||
of your team. Please wait for our moderators to manually review
|
||||
your project to see if it abides by our
|
||||
<nuxt-link class="text-link" to="/legal/rules"
|
||||
>content rules!
|
||||
</nuxt-link>
|
||||
</p>
|
||||
<p v-if="project.status === 'draft'">
|
||||
Your project is currently not viewable by people who are not part
|
||||
of your team. If you would like to publish your project, click the
|
||||
button below to send your project in for review.
|
||||
</p>
|
||||
<div v-if="project.moderator_message">
|
||||
<hr class="card-divider" />
|
||||
<div v-if="project.moderator_message.body">
|
||||
@ -260,14 +336,6 @@
|
||||
<CheckIcon />
|
||||
Resubmit for review
|
||||
</button>
|
||||
<button
|
||||
v-if="project.status === 'draft'"
|
||||
class="iconified-button brand-button"
|
||||
@click="submitForReview"
|
||||
>
|
||||
<CheckIcon />
|
||||
Submit for review
|
||||
</button>
|
||||
<button
|
||||
v-if="project.status === 'approved'"
|
||||
class="iconified-button"
|
||||
@ -550,6 +618,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<section class="normal-page__content">
|
||||
<ProjectPublishingChecklist
|
||||
:project="project"
|
||||
:versions="versions"
|
||||
:current-member="currentMember"
|
||||
:is-settings="isSettings"
|
||||
:route-name="routeName"
|
||||
:set-processing="setProcessing"
|
||||
:collapsed="collapsedChecklist"
|
||||
:toggle-collapsed="toggleChecklistCollapse"
|
||||
/>
|
||||
<div
|
||||
v-if="project.status === 'unlisted'"
|
||||
class="card warning"
|
||||
@ -618,45 +696,47 @@
|
||||
type="banner"
|
||||
small-screen="square"
|
||||
/>
|
||||
<NavRow
|
||||
:links="[
|
||||
{
|
||||
label: 'Description',
|
||||
href: `/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}`,
|
||||
},
|
||||
{
|
||||
label: 'Gallery',
|
||||
href: `/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/gallery`,
|
||||
shown: project.gallery.length > 0 || !!currentMember,
|
||||
},
|
||||
{
|
||||
label: 'Changelog',
|
||||
href: `/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/changelog`,
|
||||
shown: project.versions.length > 0,
|
||||
},
|
||||
{
|
||||
label: 'Versions',
|
||||
href: `/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/versions`,
|
||||
shown: project.versions.length > 0 || !!currentMember,
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
href: `/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/settings`,
|
||||
shown: !!currentMember,
|
||||
},
|
||||
]"
|
||||
class="card"
|
||||
/>
|
||||
<div class="navigation-card">
|
||||
<NavRow
|
||||
:links="[
|
||||
{
|
||||
label: 'Description',
|
||||
href: `/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}`,
|
||||
},
|
||||
{
|
||||
label: 'Gallery',
|
||||
href: `/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/gallery`,
|
||||
shown: project.gallery.length > 0 || !!currentMember,
|
||||
},
|
||||
{
|
||||
label: 'Changelog',
|
||||
href: `/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/changelog`,
|
||||
shown: project.versions.length > 0,
|
||||
},
|
||||
{
|
||||
label: 'Versions',
|
||||
href: `/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/versions`,
|
||||
shown: project.versions.length > 0 || !!currentMember,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<div v-if="$auth.user && currentMember" class="input-group">
|
||||
<nuxt-link
|
||||
:to="`/${project.project_type}/${project.slug}/settings`"
|
||||
class="iconified-button"
|
||||
>
|
||||
<SettingsIcon /> Settings
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
<NuxtChild
|
||||
:project.sync="project"
|
||||
:versions.sync="versions"
|
||||
@ -680,7 +760,6 @@ import UpdateIcon from '~/assets/images/utils/updated.svg?inline'
|
||||
import CodeIcon from '~/assets/images/sidebar/mod.svg?inline'
|
||||
import ReportIcon from '~/assets/images/utils/report.svg?inline'
|
||||
import HeartIcon from '~/assets/images/utils/heart.svg?inline'
|
||||
import InfoIcon from '~/assets/images/utils/info.svg?inline'
|
||||
import IssuesIcon from '~/assets/images/utils/issues.svg?inline'
|
||||
import WikiIcon from '~/assets/images/utils/wiki.svg?inline'
|
||||
import DiscordIcon from '~/assets/images/external/discord.svg?inline'
|
||||
@ -691,14 +770,27 @@ import PayPalIcon from '~/assets/images/external/paypal.svg?inline'
|
||||
import OpenCollectiveIcon from '~/assets/images/external/opencollective.svg?inline'
|
||||
import UnknownIcon from '~/assets/images/utils/unknown-donation.svg?inline'
|
||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?inline'
|
||||
import EyeIcon from '~/assets/images/utils/eye.svg?inline'
|
||||
import Advertisement from '~/components/ads/Advertisement'
|
||||
import Badge from '~/components/ui/Badge'
|
||||
import Categories from '~/components/ui/search/Categories'
|
||||
import EnvironmentIndicator from '~/components/ui/EnvironmentIndicator'
|
||||
import Modal from '~/components/ui/Modal'
|
||||
import ModalReport from '~/components/ui/ModalReport'
|
||||
import NavRow from '~/components/ui/NavRow'
|
||||
import CopyCode from '~/components/ui/CopyCode'
|
||||
import Avatar from '~/components/ui/Avatar'
|
||||
import NavStack from '~/components/ui/NavStack'
|
||||
import NavStackItem from '~/components/ui/NavStackItem'
|
||||
import ProjectPublishingChecklist from '~/components/ui/ProjectPublishingChecklist'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
||||
import UsersIcon from '~/assets/images/utils/users.svg?inline'
|
||||
import CategoriesIcon from '~/assets/images/utils/tags.svg?inline'
|
||||
import DescriptionIcon from '~/assets/images/utils/align-left.svg?inline'
|
||||
import LinksIcon from '~/assets/images/utils/link.svg?inline'
|
||||
import LicenseIcon from '~/assets/images/utils/copyright.svg?inline'
|
||||
import GalleryIcon from '~/assets/images/utils/image.svg?inline'
|
||||
import VersionIcon from '~/assets/images/utils/version.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -709,6 +801,8 @@ export default {
|
||||
Advertisement,
|
||||
Modal,
|
||||
ModalReport,
|
||||
ProjectPublishingChecklist,
|
||||
EnvironmentIndicator,
|
||||
IssuesIcon,
|
||||
DownloadIcon,
|
||||
CalendarIcon,
|
||||
@ -718,7 +812,6 @@ export default {
|
||||
CodeIcon,
|
||||
ReportIcon,
|
||||
HeartIcon,
|
||||
InfoIcon,
|
||||
WikiIcon,
|
||||
DiscordIcon,
|
||||
BuyMeACoffeeLogo,
|
||||
@ -729,6 +822,17 @@ export default {
|
||||
PatreonIcon,
|
||||
KoFiIcon,
|
||||
ChevronRightIcon,
|
||||
NavStack,
|
||||
NavStackItem,
|
||||
SettingsIcon,
|
||||
EyeIcon,
|
||||
GalleryIcon,
|
||||
VersionIcon,
|
||||
UsersIcon,
|
||||
CategoriesIcon,
|
||||
DescriptionIcon,
|
||||
LinksIcon,
|
||||
LicenseIcon,
|
||||
},
|
||||
async asyncData(data) {
|
||||
try {
|
||||
@ -868,9 +972,14 @@ export default {
|
||||
return {
|
||||
showKnownErrors: false,
|
||||
licenseText: '',
|
||||
isSettings: false,
|
||||
routeName: '',
|
||||
from: '',
|
||||
collapsedChecklist: false,
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.reset()
|
||||
this.versions = this.$computeVersions(this.versions)
|
||||
this.featuredVersions = this.$computeVersions(this.featuredVersions)
|
||||
},
|
||||
@ -948,8 +1057,26 @@ export default {
|
||||
return id
|
||||
}
|
||||
},
|
||||
featuredGalleryImage() {
|
||||
return this.project.gallery.find((img) => img.featured)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'$route.path': {
|
||||
async handler() {
|
||||
await this.reset()
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reset() {
|
||||
// First time going to settings, this will run, but not subsequent times.
|
||||
if (!this.isSettings) {
|
||||
this.from = this.$nuxt.context.from ? this.$nuxt.context.from.name : ''
|
||||
}
|
||||
this.routeName = this.$route.name
|
||||
this.isSettings = this.routeName.startsWith('type-id-settings')
|
||||
},
|
||||
async resetProject() {
|
||||
const project = (
|
||||
await this.$axios.get(
|
||||
@ -1018,36 +1145,43 @@ export default {
|
||||
async submitForReview() {
|
||||
if (
|
||||
this.project.body === '' ||
|
||||
this.project.body.startsWith('# Placeholder description') ||
|
||||
this.versions.length < 1 ||
|
||||
this.project.client_side === 'unknown' ||
|
||||
this.project.server_side === 'unknown'
|
||||
) {
|
||||
this.showKnownErrors = true
|
||||
} else {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
await this.$axios.patch(
|
||||
`project/${this.project.id}`,
|
||||
{
|
||||
status: 'processing',
|
||||
},
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
|
||||
this.project.status = 'processing'
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
await this.setProcessing()
|
||||
}
|
||||
},
|
||||
toggleChecklistCollapse() {
|
||||
this.collapsedChecklist = !this.collapsedChecklist
|
||||
},
|
||||
async setProcessing() {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
await this.$axios.patch(
|
||||
`project/${this.project.id}`,
|
||||
{
|
||||
status: 'processing',
|
||||
},
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
|
||||
this.project.status = 'processing'
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
async getLicenseData() {
|
||||
try {
|
||||
const text = await this.$axios.get(
|
||||
@ -1060,6 +1194,104 @@ export default {
|
||||
|
||||
this.$refs.modal_license.show()
|
||||
},
|
||||
async patchProject(data, quiet = false) {
|
||||
let result = false
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
await this.$axios.patch(
|
||||
`project/${this.project.id}`,
|
||||
data,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
|
||||
if (this.iconChanged) {
|
||||
await this.$axios.patch(
|
||||
`project/${this.project.id}/icon?ext=${
|
||||
this.icon.type.split('/')[this.icon.type.split('/').length - 1]
|
||||
}`,
|
||||
this.icon,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
}
|
||||
|
||||
for (const key in data) {
|
||||
this.project[key] = data[key]
|
||||
}
|
||||
|
||||
if (data.license_id) {
|
||||
this.project.license.id = data.license_id
|
||||
}
|
||||
if (data.license_url) {
|
||||
this.project.license.url = data.license_url
|
||||
}
|
||||
|
||||
this.$emit('update:project', this.project)
|
||||
result = true
|
||||
if (!quiet) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'Project updated',
|
||||
text: 'Your project has been updated.',
|
||||
type: 'success',
|
||||
})
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
|
||||
return result
|
||||
},
|
||||
async patchIcon(icon) {
|
||||
let result = false
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
await this.$axios.patch(
|
||||
`project/${this.project.id}/icon?ext=${
|
||||
icon.type.split('/')[icon.type.split('/').length - 1]
|
||||
}`,
|
||||
icon,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
await this.updateIcon()
|
||||
result = true
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'Project icon updated',
|
||||
text: "Your project's icon has been updated.",
|
||||
type: 'success',
|
||||
})
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
return result
|
||||
},
|
||||
async updateIcon() {
|
||||
const response = await this.$axios.get(
|
||||
`project/${this.project.id}`,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
this.project.icon_url = response.data.icon_url
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1068,23 +1300,13 @@ export default {
|
||||
grid-area: header;
|
||||
.title {
|
||||
overflow-wrap: break-word;
|
||||
margin: 0.25rem 0;
|
||||
margin: var(--spacing-card-xs) 0;
|
||||
color: var(--color-text-dark);
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
.side-descriptor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-text-dark);
|
||||
font-weight: bold;
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
svg {
|
||||
height: 1.25rem;
|
||||
margin-right: 0.125rem;
|
||||
}
|
||||
.status-badge {
|
||||
margin-top: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.description {
|
||||
@ -1125,6 +1347,38 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.project__header {
|
||||
overflow: hidden;
|
||||
.project__gallery {
|
||||
display: none;
|
||||
}
|
||||
&.has-featured-image {
|
||||
.project__gallery {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
background-color: var(--color-button-bg-active);
|
||||
img {
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
.project__icon {
|
||||
margin-top: calc(-3rem - var(--spacing-card-lg) - 4px);
|
||||
margin-left: -4px;
|
||||
z-index: 1;
|
||||
border: 4px solid var(--color-raised-bg);
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
.project__header__content {
|
||||
margin: 0;
|
||||
background: none;
|
||||
border-radius: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.project-info {
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
@ -1305,4 +1559,23 @@ export default {
|
||||
.modal-license {
|
||||
padding: var(--spacing-card-bg);
|
||||
}
|
||||
.settings-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-card-sm);
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-card-bg);
|
||||
|
||||
.settings-header__icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.settings-header__text {
|
||||
h1 {
|
||||
font-size: var(--font-size-md);
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--spacing-card-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
144
pages/_type/_id/settings/description.vue
Normal file
@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<label for="project-description">
|
||||
<span class="label__title size-card-header">Description</span>
|
||||
<span class="label__description">
|
||||
You can type an extended description of your mod here. This editor
|
||||
supports
|
||||
<a
|
||||
class="text-link"
|
||||
href="https://guides.github.com/features/mastering-markdown/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Markdown</a
|
||||
>. HTML can also be used inside your description, not including
|
||||
styles, scripts, and iframes (though YouTube iframes are allowed).
|
||||
<span class="label__subdescription">
|
||||
The description must clearly and honestly describe the purpose and
|
||||
function of the project. See section 2.1 of the
|
||||
<nuxt-link to="/legal/rules" class="text-link" target="_blank"
|
||||
>Content Rules</nuxt-link
|
||||
>
|
||||
for the full requirements.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<Chips v-model="bodyViewMode" :items="['source', 'preview']" />
|
||||
<div v-if="bodyViewMode === 'source'" class="resizable-textarea-wrapper">
|
||||
<textarea
|
||||
id="project-description"
|
||||
v-model="description"
|
||||
:disabled="(currentMember.permissions & EDIT_BODY) !== EDIT_BODY"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="bodyViewMode === 'preview'"
|
||||
v-highlightjs
|
||||
class="markdown-body"
|
||||
v-html="
|
||||
description ? $xss($md.render(description)) : 'No body specified.'
|
||||
"
|
||||
></div>
|
||||
<div class="input-group">
|
||||
<button
|
||||
type="button"
|
||||
class="iconified-button brand-button"
|
||||
:disabled="!hasChanges"
|
||||
@click="saveChanges()"
|
||||
>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Chips from '~/components/ui/Chips'
|
||||
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Chips,
|
||||
SaveIcon,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
allMembers: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
return null
|
||||
},
|
||||
},
|
||||
patchProject: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'Patch project function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
description: '',
|
||||
bodyViewMode: 'source',
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.description = this.project.body
|
||||
},
|
||||
computed: {
|
||||
patchData() {
|
||||
const data = {}
|
||||
|
||||
if (this.description !== this.project.body) {
|
||||
data.body = this.description
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
hasChanges() {
|
||||
return Object.keys(this.patchData).length > 0
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.EDIT_BODY = 1 << 3
|
||||
},
|
||||
methods: {
|
||||
saveChanges() {
|
||||
if (this.hasChanges) {
|
||||
this.patchProject(this.patchData)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.resizable-textarea-wrapper textarea {
|
||||
min-height: 40rem;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
}
|
||||
</style>
|
||||
419
pages/_type/_id/settings/index.vue
Normal file
@ -0,0 +1,419 @@
|
||||
<template>
|
||||
<div>
|
||||
<ModalConfirm
|
||||
ref="modal_confirm"
|
||||
title="Are you sure you want to delete this project?"
|
||||
description="If you proceed, all versions and any attached data will be removed from our servers. This may break other projects, so be careful."
|
||||
:has-to-type="true"
|
||||
:confirmation-text="project.title"
|
||||
proceed-label="Delete"
|
||||
@proceed="deleteProject"
|
||||
/>
|
||||
<section class="universal-card">
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Project information</span>
|
||||
</h3>
|
||||
</div>
|
||||
<label for="project-name">
|
||||
<span class="label__title">Icon</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<Avatar
|
||||
:src="
|
||||
deletedIcon ? null : previewImage ? previewImage : project.icon_url
|
||||
"
|
||||
:alt="project.title"
|
||||
size="md"
|
||||
class="project__icon"
|
||||
/>
|
||||
<div class="input-stack">
|
||||
<FileInput
|
||||
:max-size="262144"
|
||||
:show-icon="true"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
class="choose-image iconified-button"
|
||||
prompt="Upload icon"
|
||||
:disabled="!hasPermission"
|
||||
@change="showPreviewImage"
|
||||
>
|
||||
<UploadIcon />
|
||||
</FileInput>
|
||||
<button
|
||||
v-if="!deletedIcon && (previewImage || project.icon_url)"
|
||||
class="iconified-button"
|
||||
:disabled="!hasPermission"
|
||||
@click="markIconForDeletion"
|
||||
>
|
||||
<TrashIcon />
|
||||
Remove icon
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="project-name">
|
||||
<span class="label__title">Name</span>
|
||||
</label>
|
||||
<input
|
||||
id="project-name"
|
||||
v-model="name"
|
||||
maxlength="2048"
|
||||
type="text"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
|
||||
<label for="project-slug">
|
||||
<span class="label__title">URL</span>
|
||||
</label>
|
||||
<div class="text-input-wrapper">
|
||||
<div class="text-input-wrapper__before">https://modrinth.com/mod/</div>
|
||||
<input
|
||||
id="project-slug"
|
||||
v-model="slug"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label for="project-summary">
|
||||
<span class="label__title">Summary</span>
|
||||
</label>
|
||||
<div class="textarea-wrapper summary-input">
|
||||
<textarea
|
||||
id="project-summary"
|
||||
v-model="summary"
|
||||
maxlength="256"
|
||||
:disabled="!hasPermission"
|
||||
></textarea>
|
||||
</div>
|
||||
<template
|
||||
v-if="
|
||||
project.project_type !== 'resourcepack' &&
|
||||
project.project_type !== 'plugin' &&
|
||||
project.project_type !== 'shader' &&
|
||||
project.project_type !== 'datapack'
|
||||
"
|
||||
>
|
||||
<div class="adjacent-input">
|
||||
<label for="project-env-client">
|
||||
<span class="label__title">Client-side</span>
|
||||
<span class="label__description">
|
||||
Select based on if the
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }} has
|
||||
functionality on the client side. Just because a mod works in
|
||||
Singleplayer doesn't mean it has actual client-side functionality.
|
||||
</span>
|
||||
</label>
|
||||
<Multiselect
|
||||
id="project-env-client"
|
||||
v-model="clientSide"
|
||||
placeholder="Select one"
|
||||
:options="sideTypes"
|
||||
:custom-label="
|
||||
(value) => value.charAt(0).toUpperCase() + value.slice(1)
|
||||
"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="project-env-server">
|
||||
<span class="label__title">Server-side</span>
|
||||
<span class="label__description">
|
||||
Select based on if the
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }} has
|
||||
functionality on the <strong>logical</strong> server. Remember
|
||||
that Singleplayer contains an integrated server.
|
||||
</span>
|
||||
</label>
|
||||
<Multiselect
|
||||
id="project-env-server"
|
||||
v-model="serverSide"
|
||||
placeholder="Select one"
|
||||
:options="sideTypes"
|
||||
:custom-label="
|
||||
(value) => value.charAt(0).toUpperCase() + value.slice(1)
|
||||
"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="adjacent-input">
|
||||
<label for="project-visibility">
|
||||
<span class="label__title">Visibility</span>
|
||||
<span class="label__description">
|
||||
Set the visibility of your project. Listed and archived projects are
|
||||
visible in search. Unlisted projects are published, but not visible
|
||||
in search or on user profiles. Private projects are only accessible
|
||||
by members of the project.
|
||||
</span>
|
||||
</label>
|
||||
<Multiselect
|
||||
id="project-visibility"
|
||||
v-model="visibility"
|
||||
placeholder="Select one"
|
||||
:options="statusOptions"
|
||||
:custom-label="(value) => $formatProjectStatus(value)"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button
|
||||
type="button"
|
||||
class="iconified-button brand-button"
|
||||
:disabled="!hasChanges"
|
||||
@click="saveChanges()"
|
||||
>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="universal-card">
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Delete project</span>
|
||||
</h3>
|
||||
</div>
|
||||
<p>
|
||||
Removes your project from Modrinth's servers and search. Clicking on
|
||||
this will delete your project, so be extra careful!
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="iconified-button danger-button"
|
||||
:disabled="!hasDeletePermission"
|
||||
@click="$refs.modal_confirm.show()"
|
||||
>
|
||||
<TrashIcon />
|
||||
Delete project
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import Avatar from '~/components/ui/Avatar'
|
||||
import ModalConfirm from '~/components/ui/ModalConfirm'
|
||||
import FileInput from '~/components/ui/FileInput'
|
||||
|
||||
import UploadIcon from '~/assets/images/utils/upload.svg?inline'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
ModalConfirm,
|
||||
FileInput,
|
||||
Multiselect,
|
||||
UploadIcon,
|
||||
SaveIcon,
|
||||
TrashIcon,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
return null
|
||||
},
|
||||
},
|
||||
patchProject: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'Patch project function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
patchIcon: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'Patch icon function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
updateIcon: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'Update icon function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
slug: '',
|
||||
summary: '',
|
||||
icon: null,
|
||||
previewImage: null,
|
||||
clientSide: '',
|
||||
serverSide: '',
|
||||
deletedIcon: false,
|
||||
visibility: '',
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.name = this.project.title
|
||||
this.slug = this.project.slug
|
||||
this.summary = this.project.description
|
||||
this.clientSide = this.project.client_side
|
||||
this.serverSide = this.project.server_side
|
||||
this.visibility = this.statusOptions.includes(this.project.status)
|
||||
? this.project.status
|
||||
: this.project.requested_status
|
||||
},
|
||||
computed: {
|
||||
hasPermission() {
|
||||
const EDIT_DETAILS = 1 << 2
|
||||
return (this.currentMember.permissions & EDIT_DETAILS) === EDIT_DETAILS
|
||||
},
|
||||
hasDeletePermission() {
|
||||
const DELETE_PROJECT = 1 << 7
|
||||
return (
|
||||
(this.currentMember.permissions & DELETE_PROJECT) === DELETE_PROJECT
|
||||
)
|
||||
},
|
||||
sideTypes() {
|
||||
return ['required', 'optional', 'unsupported']
|
||||
},
|
||||
statusOptions() {
|
||||
return ['approved', 'archived', 'unlisted', 'private']
|
||||
},
|
||||
patchData() {
|
||||
const data = {}
|
||||
|
||||
if (this.name !== this.project.title) {
|
||||
data.title = this.name
|
||||
}
|
||||
if (this.slug !== this.project.slug) {
|
||||
data.slug = this.slug
|
||||
}
|
||||
if (this.summary !== this.project.description) {
|
||||
data.description = this.summary
|
||||
}
|
||||
if (this.clientSide !== this.project.client_side) {
|
||||
data.client_side = this.clientSide
|
||||
}
|
||||
if (this.serverSide !== this.project.server_side) {
|
||||
data.server_side = this.serverSide
|
||||
}
|
||||
if (this.visibility !== this.project.requested_status) {
|
||||
if (!this.statusOptions.includes(this.project.status)) {
|
||||
data.requested_status = this.visibility
|
||||
} else {
|
||||
data.status = this.visibility
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
hasChanges() {
|
||||
return (
|
||||
Object.keys(this.patchData).length > 0 || this.deletedIcon || this.icon
|
||||
)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async saveChanges() {
|
||||
if (this.hasChanges) {
|
||||
await this.patchProject(this.patchData)
|
||||
}
|
||||
|
||||
if (this.deletedIcon) {
|
||||
await this.deleteIcon()
|
||||
this.deletedIcon = false
|
||||
} else if (this.icon) {
|
||||
await this.patchIcon(this.icon)
|
||||
this.icon = null
|
||||
}
|
||||
},
|
||||
showPreviewImage(files) {
|
||||
const reader = new FileReader()
|
||||
this.icon = files[0]
|
||||
this.deletedIcon = false
|
||||
reader.readAsDataURL(this.icon)
|
||||
reader.onload = (event) => {
|
||||
this.previewImage = event.target.result
|
||||
}
|
||||
},
|
||||
async deleteProject() {
|
||||
await this.$axios.delete(
|
||||
`project/${this.project.id}`,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
await this.$store.dispatch('user/fetchProjects')
|
||||
await this.$router.push(`/dashboard/projects`)
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'Project deleted',
|
||||
text: 'Your project has been deleted.',
|
||||
type: 'success',
|
||||
})
|
||||
},
|
||||
markIconForDeletion() {
|
||||
this.deletedIcon = true
|
||||
this.icon = null
|
||||
this.previewImage = null
|
||||
},
|
||||
async deleteIcon() {
|
||||
await this.$axios.delete(
|
||||
`project/${this.project.id}/icon`,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
await this.updateIcon()
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'Project icon removed',
|
||||
text: "Your project's icon has been removed.",
|
||||
type: 'success',
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.summary-input {
|
||||
min-height: 8rem;
|
||||
max-width: 24rem;
|
||||
}
|
||||
</style>
|
||||
286
pages/_type/_id/settings/license.vue
Normal file
@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<div class="adjacent-input">
|
||||
<label for="license-multiselect">
|
||||
<span class="label__title size-card-header">License</span>
|
||||
<span class="label__description">
|
||||
It is very important to choose a proper license for your
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }}. You
|
||||
may choose one from our list or provide a custom license. You may
|
||||
also provide a custom URL to your chosen license; otherwise, the
|
||||
license text will be displayed.
|
||||
<span
|
||||
v-if="license && license.friendly === 'Custom'"
|
||||
class="label__subdescription"
|
||||
>
|
||||
Enter a valid
|
||||
<a
|
||||
href="https://spdx.org/licenses/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-link"
|
||||
>
|
||||
SPDX license identifier</a
|
||||
>
|
||||
in the marked area. If your license does not have a SPDX
|
||||
identifier (for example, if you created the license yourself or if
|
||||
the license is Minecraft-specific), simply check the box and enter
|
||||
the name of the license instead.
|
||||
</span>
|
||||
<span class="label__subdescription">
|
||||
Confused? See our
|
||||
<a
|
||||
href="https://blog.modrinth.com/licensing-guide/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-link"
|
||||
>
|
||||
licensing guide</a
|
||||
>
|
||||
for more information.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="input-stack">
|
||||
<Multiselect
|
||||
id="license-multiselect"
|
||||
v-model="license"
|
||||
placeholder="Select license..."
|
||||
track-by="short"
|
||||
label="friendly"
|
||||
:options="defaultLicenses"
|
||||
:searchable="true"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:class="{
|
||||
'known-error': license.short === '' && showKnownErrors,
|
||||
}"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
<Checkbox
|
||||
v-if="license.requiresOnlyOrLater"
|
||||
v-model="allowOrLater"
|
||||
:disabled="!hasPermission"
|
||||
>
|
||||
Allow later editions of this license
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
v-if="license.friendly === 'Custom'"
|
||||
v-model="nonSpdxLicense"
|
||||
:disabled="!hasPermission"
|
||||
>
|
||||
License does not have a SPDX identifier
|
||||
</Checkbox>
|
||||
<input
|
||||
v-if="license.friendly === 'Custom'"
|
||||
v-model="license.short"
|
||||
type="text"
|
||||
maxlength="2048"
|
||||
:placeholder="nonSpdxLicense ? 'License name' : 'SPDX identifier'"
|
||||
:class="{
|
||||
'known-error': license.short === '' && showKnownErrors,
|
||||
}"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
<input
|
||||
v-model="licenseUrl"
|
||||
type="url"
|
||||
maxlength="2048"
|
||||
placeholder="License URL (optional)"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-stack">
|
||||
<button
|
||||
type="button"
|
||||
class="iconified-button brand-button"
|
||||
:disabled="!hasChanges"
|
||||
@click="saveChanges()"
|
||||
>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import Checkbox from '~/components/ui/Checkbox'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Multiselect,
|
||||
Checkbox,
|
||||
SaveIcon,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
return null
|
||||
},
|
||||
},
|
||||
patchProject: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'Patch project function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
licenseUrl: '',
|
||||
license: { friendly: '', short: '', requiresOnlyOrLater: false },
|
||||
allowOrLater: false,
|
||||
nonSpdxLicense: false,
|
||||
showKnownErrors: false,
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.licenseUrl = this.project.license.url
|
||||
|
||||
const licenseId = this.project.license.id
|
||||
const trimmedLicenseId = licenseId
|
||||
.replaceAll('-only', '')
|
||||
.replaceAll('-or-later', '')
|
||||
.replaceAll('LicenseRef-', '')
|
||||
this.license = this.defaultLicenses.find(
|
||||
(x) => x.short === trimmedLicenseId
|
||||
) ?? {
|
||||
friendly: 'Custom',
|
||||
short: licenseId.replaceAll('LicenseRef-', ''),
|
||||
}
|
||||
this.allowOrLater = licenseId.includes('-or-later')
|
||||
this.nonSpdxLicense = licenseId.includes('LicenseRef-')
|
||||
},
|
||||
computed: {
|
||||
hasPermission() {
|
||||
const EDIT_DETAILS = 1 << 2
|
||||
return (this.currentMember.permissions & EDIT_DETAILS) === EDIT_DETAILS
|
||||
},
|
||||
licenseId() {
|
||||
let id = ''
|
||||
if (this.nonSpdxLicense || this.license.short === 'All-Rights-Reserved')
|
||||
id += 'LicenseRef-'
|
||||
id += this.license.short
|
||||
if (this.license.requiresOnlyOrLater)
|
||||
id += this.allowOrLater ? 'or-later' : '-only'
|
||||
if (this.nonSpdxLicense) id.replaceAll(' ', '-')
|
||||
return id
|
||||
},
|
||||
defaultLicenses() {
|
||||
return [
|
||||
{ friendly: 'Custom', short: '' },
|
||||
{
|
||||
friendly: 'All Rights Reserved/No License',
|
||||
short: 'All-Rights-Reserved',
|
||||
},
|
||||
{ friendly: 'Apache License 2.0', short: 'Apache-2.0' },
|
||||
{
|
||||
friendly: 'BSD 2-Clause "Simplified" License',
|
||||
short: 'BSD-2-Clause',
|
||||
},
|
||||
{
|
||||
friendly: 'BSD 3-Clause "New" or "Revised" License',
|
||||
short: 'BSD-3-Clause',
|
||||
},
|
||||
{
|
||||
friendly: 'CC Zero (Public Domain equivalent)',
|
||||
short: 'CC0-1.0',
|
||||
},
|
||||
{ friendly: 'CC-BY 4.0', short: 'CC-BY-4.0' },
|
||||
{
|
||||
friendly: 'CC-BY-SA 4.0',
|
||||
short: 'CC-BY-SA-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'CC-BY-NC 4.0',
|
||||
short: 'CC-BY-NC-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'CC-BY-NC-SA 4.0',
|
||||
short: 'CC-BY-NC-SA-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'CC-BY-ND 4.0',
|
||||
short: 'CC-BY-ND-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'CC-BY-NC-ND 4.0',
|
||||
short: 'CC-BY-NC-ND-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'GNU Affero General Public License v3',
|
||||
short: 'AGPL-3.0',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: 'GNU Lesser General Public License v2.1',
|
||||
short: 'LGPL-2.1',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: 'GNU Lesser General Public License v3',
|
||||
short: 'LGPL-3.0',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: 'GNU General Public License v2',
|
||||
short: 'GPL-2.0',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: 'GNU General Public License v3',
|
||||
short: 'GPL-3.0',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{ friendly: 'ISC License', short: 'ISC' },
|
||||
{ friendly: 'MIT License', short: 'MIT' },
|
||||
{ friendly: 'Mozilla Public License 2.0', short: 'MPL-2.0' },
|
||||
{ friendly: 'zlib License', short: 'Zlib' },
|
||||
]
|
||||
},
|
||||
patchData() {
|
||||
const data = {}
|
||||
|
||||
if (this.licenseId !== this.project.license.id) {
|
||||
data.license_id = this.licenseId
|
||||
data.license_url = this.licenseUrl ? this.licenseUrl : null
|
||||
} else if (this.licenseUrl !== this.project.license.url) {
|
||||
data.license_url = this.licenseUrl ? this.licenseUrl : null
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
hasChanges() {
|
||||
return Object.keys(this.patchData).length > 0
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
saveChanges() {
|
||||
if (this.hasChanges) {
|
||||
this.patchProject(this.patchData)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
286
pages/_type/_id/settings/links.vue
Normal file
@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<h2>External links</h2>
|
||||
<div class="adjacent-input">
|
||||
<label
|
||||
id="project-issue-tracker"
|
||||
title="A place for users to report bugs, issues, and concerns about your project."
|
||||
>
|
||||
<span class="label__title">Issue tracker</span>
|
||||
<span class="label__description">
|
||||
A place for users to report bugs, issues, and concerns about your
|
||||
project.
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="project-issue-tracker"
|
||||
v-model="issuesUrl"
|
||||
type="url"
|
||||
placeholder="Enter a valid URL"
|
||||
maxlength="2048"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label
|
||||
id="project-source-code"
|
||||
title="A page/repository containing the source code for your project"
|
||||
>
|
||||
<span class="label__title">Source code</span>
|
||||
<span class="label__description">
|
||||
A page/repository containing the source code for your project
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="project-source-code"
|
||||
v-model="sourceUrl"
|
||||
type="url"
|
||||
maxlength="2048"
|
||||
placeholder="Enter a valid URL"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label
|
||||
id="project-wiki-page"
|
||||
title="A page containing information, documentation, and help for the project."
|
||||
>
|
||||
<span class="label__title">Wiki page</span>
|
||||
<span class="label__description">
|
||||
A page containing information, documentation, and help for the
|
||||
project.
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="project-wiki-page"
|
||||
v-model="wikiUrl"
|
||||
type="url"
|
||||
maxlength="2048"
|
||||
placeholder="Enter a valid URL"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label
|
||||
id="project-discord-invite"
|
||||
title="An invitation link to your Discord server."
|
||||
>
|
||||
<span class="label__title">Discord invite</span>
|
||||
<span class="label__description">
|
||||
An invitation link to your Discord server.
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="project-discord-invite"
|
||||
v-model="discordUrl"
|
||||
type="url"
|
||||
maxlength="2048"
|
||||
placeholder="Enter a valid URL"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
</div>
|
||||
<span class="label">
|
||||
<span class="label__title">Donation links</span>
|
||||
<span class="label__description">
|
||||
Add donation links for users to support you directly.
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div
|
||||
v-for="(donationLink, index) in donationLinks"
|
||||
:key="`donation-link-${index}`"
|
||||
class="input-group donation-link-group"
|
||||
>
|
||||
<Multiselect
|
||||
v-model="donationLink.platform"
|
||||
placeholder="Select platform"
|
||||
:options="$tag.donationPlatforms.map((x) => x.name)"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:disabled="!hasPermission"
|
||||
@input="updateDonationLinks"
|
||||
/>
|
||||
<input
|
||||
v-model="donationLink.url"
|
||||
type="url"
|
||||
maxlength="2048"
|
||||
placeholder="Enter a valid URL"
|
||||
:disabled="!hasPermission"
|
||||
@input="updateDonationLinks"
|
||||
/>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button
|
||||
type="button"
|
||||
class="iconified-button brand-button"
|
||||
:disabled="!hasChanges"
|
||||
@click="saveChanges()"
|
||||
>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Multiselect,
|
||||
SaveIcon,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
return null
|
||||
},
|
||||
},
|
||||
patchProject: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'Patch project function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
issuesUrl: '',
|
||||
sourceUrl: '',
|
||||
wikiUrl: '',
|
||||
discordUrl: '',
|
||||
|
||||
donationLinks: [],
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.issuesUrl = this.project.issues_url
|
||||
this.sourceUrl = this.project.source_url
|
||||
this.wikiUrl = this.project.wiki_url
|
||||
this.discordUrl = this.project.discord_url
|
||||
|
||||
this.resetDonationLinks()
|
||||
},
|
||||
computed: {
|
||||
hasPermission() {
|
||||
const EDIT_DETAILS = 1 << 2
|
||||
return (this.currentMember.permissions & EDIT_DETAILS) === EDIT_DETAILS
|
||||
},
|
||||
patchData() {
|
||||
const data = {}
|
||||
|
||||
if (this.checkDifference(this.issuesUrl, this.project.issues_url)) {
|
||||
data.issues_url = this.issuesUrl
|
||||
}
|
||||
if (this.checkDifference(this.sourceUrl, this.project.source_url)) {
|
||||
data.source_url = this.sourceUrl
|
||||
}
|
||||
if (this.checkDifference(this.wikiUrl, this.project.wiki_url)) {
|
||||
data.wiki_url = this.wikiUrl
|
||||
}
|
||||
if (this.checkDifference(this.discordUrl, this.project.discord_url)) {
|
||||
data.discord_url = this.discordUrl
|
||||
}
|
||||
|
||||
const donationLinks = this.donationLinks.filter(
|
||||
(link) => link.url && link.platform
|
||||
)
|
||||
donationLinks.forEach((link) => {
|
||||
link.id = this.$tag.donationPlatforms.find(
|
||||
(platform) => platform.name === link.platform
|
||||
).short
|
||||
})
|
||||
if (
|
||||
donationLinks !== this.project.donation_urls &&
|
||||
!(
|
||||
this.project.donation_urls &&
|
||||
this.project.donation_urls.length === 0 &&
|
||||
donationLinks.length === 0
|
||||
)
|
||||
) {
|
||||
data.donation_urls = donationLinks
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
hasChanges() {
|
||||
return Object.keys(this.patchData).length > 0
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async saveChanges() {
|
||||
if (this.patchData && (await this.patchProject(this.patchData))) {
|
||||
this.resetDonationLinks()
|
||||
}
|
||||
},
|
||||
updateDonationLinks() {
|
||||
this.donationLinks.forEach((link) => {
|
||||
if (link.url) {
|
||||
const url = link.url.toLowerCase()
|
||||
if (url.includes('patreon.com')) {
|
||||
link.platform = 'Patreon'
|
||||
} else if (url.includes('ko-fi.com')) {
|
||||
link.platform = 'Ko-fi'
|
||||
} else if (url.includes('paypal.com')) {
|
||||
link.platform = 'Paypal'
|
||||
} else if (url.includes('buymeacoffee.com')) {
|
||||
link.platform = 'Buy Me a Coffee'
|
||||
} else if (url.includes('github.com/sponsors')) {
|
||||
link.platform = 'GitHub Sponsors'
|
||||
}
|
||||
}
|
||||
})
|
||||
if (!this.donationLinks.find((link) => !(link.url && link.platform))) {
|
||||
this.donationLinks.push({
|
||||
id: null,
|
||||
platform: null,
|
||||
url: null,
|
||||
})
|
||||
}
|
||||
},
|
||||
resetDonationLinks() {
|
||||
this.donationLinks = JSON.parse(
|
||||
JSON.stringify(this.project.donation_urls)
|
||||
)
|
||||
this.donationLinks.push({
|
||||
id: null,
|
||||
platform: null,
|
||||
url: null,
|
||||
})
|
||||
},
|
||||
checkDifference(a, b) {
|
||||
if (!a && !b) {
|
||||
return false
|
||||
}
|
||||
return a !== b
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.donation-link-group {
|
||||
input {
|
||||
flex-grow: 2;
|
||||
max-width: 26rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,77 +1,35 @@
|
||||
<template>
|
||||
<div>
|
||||
<ModalConfirm
|
||||
ref="modal_confirm"
|
||||
title="Are you sure you want to delete this project?"
|
||||
description="If you proceed, all versions and any attached data will be removed from our servers. This may break other projects, so be careful."
|
||||
:has-to-type="true"
|
||||
:confirmation-text="project.title"
|
||||
proceed-label="Delete"
|
||||
@proceed="deleteProject"
|
||||
/>
|
||||
<div class="universal-card">
|
||||
<h2>General settings</h2>
|
||||
<div class="adjacent-input">
|
||||
<label>
|
||||
<span class="label__title">Edit project information</span>
|
||||
<span class="label__description">
|
||||
Edit your project's name, description, categories, and more.
|
||||
</span>
|
||||
</label>
|
||||
<nuxt-link class="iconified-button" to="edit"
|
||||
><EditIcon />Edit</nuxt-link
|
||||
>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Manage members</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<span class="label">
|
||||
<span class="label__title">Delete project</span>
|
||||
<span class="label__description">
|
||||
Removes your project from Modrinth's servers and search. Clicking on
|
||||
this will delete your project, so be extra careful!
|
||||
</span>
|
||||
<span class="label">
|
||||
<span class="label__title">Invite a member</span>
|
||||
<span class="label__description">
|
||||
Enter the Modrinth username of the person you'd like to invite to be a
|
||||
member of this project.
|
||||
</span>
|
||||
<button
|
||||
class="iconified-button danger-button"
|
||||
:disabled="
|
||||
(currentMember.permissions & DELETE_PROJECT) !== DELETE_PROJECT
|
||||
"
|
||||
@click="$refs.modal_confirm.show()"
|
||||
>
|
||||
<TrashIcon />Delete project
|
||||
</span>
|
||||
<div
|
||||
v-if="(currentMember.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
||||
class="input-group"
|
||||
>
|
||||
<input
|
||||
id="username"
|
||||
v-model="currentUsername"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
/>
|
||||
<label for="username" class="hidden">Username</label>
|
||||
<button class="iconified-button brand-button" @click="inviteTeamMember">
|
||||
<UserPlusIcon />
|
||||
Invite
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="universal-card">
|
||||
<h2>Manage members</h2>
|
||||
<div class="adjacent-input">
|
||||
<span class="label">
|
||||
<span class="label__title">Invite a member</span>
|
||||
<span class="label__description">
|
||||
Enter the Modrinth username of the person you'd like to invite to be
|
||||
a member of this project.
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
v-if="(currentMember.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
||||
class="input-group"
|
||||
>
|
||||
<input
|
||||
id="username"
|
||||
v-model="currentUsername"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
/>
|
||||
<label for="username" class="hidden">Username</label>
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
@click="inviteTeamMember"
|
||||
>
|
||||
<PlusIcon />
|
||||
Invite
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(member, index) in allTeamMembers"
|
||||
:key="member.user.id"
|
||||
@ -94,10 +52,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-buttons">
|
||||
<Badge v-if="member.accepted" type="accepted" color="green" />
|
||||
<Badge v-else type="pending" color="orange" />
|
||||
<Badge v-if="member.accepted" type="accepted" />
|
||||
<Badge v-else type="pending" />
|
||||
<button
|
||||
class="dropdown-icon"
|
||||
class="square-button dropdown-icon"
|
||||
@click="
|
||||
openTeamMembers.indexOf(member.user.id) === -1
|
||||
? openTeamMembers.push(member.user.id)
|
||||
@ -250,16 +208,26 @@
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="button-group push-right">
|
||||
<div class="input-group">
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
@click="updateTeamMember(index)"
|
||||
>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</button>
|
||||
<button
|
||||
v-if="member.oldRole !== 'Owner'"
|
||||
class="iconified-button"
|
||||
class="iconified-button danger-button"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
@click="removeTeamMember(index)"
|
||||
>
|
||||
<TrashIcon />
|
||||
<UserRemoveIcon />
|
||||
Remove member
|
||||
</button>
|
||||
<button
|
||||
@ -271,19 +239,9 @@
|
||||
class="iconified-button"
|
||||
@click="transferOwnership(index)"
|
||||
>
|
||||
<UserIcon />
|
||||
<TransferIcon />
|
||||
Transfer ownership
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
@click="updateTeamMember(index)"
|
||||
>
|
||||
<CheckIcon />
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -291,30 +249,26 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ModalConfirm from '~/components/ui/ModalConfirm'
|
||||
import Checkbox from '~/components/ui/Checkbox'
|
||||
import Badge from '~/components/ui/Badge'
|
||||
|
||||
import DropdownIcon from '~/assets/images/utils/dropdown.svg?inline'
|
||||
import PlusIcon from '~/assets/images/utils/plus.svg?inline'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import UserIcon from '~/assets/images/utils/user.svg?inline'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
import TransferIcon from '~/assets/images/utils/transfer.svg?inline'
|
||||
import UserPlusIcon from '~/assets/images/utils/user-plus.svg?inline'
|
||||
import UserRemoveIcon from '~/assets/images/utils/user-x.svg?inline'
|
||||
import Avatar from '~/components/ui/Avatar'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
DropdownIcon,
|
||||
ModalConfirm,
|
||||
Checkbox,
|
||||
Badge,
|
||||
PlusIcon,
|
||||
CheckIcon,
|
||||
EditIcon,
|
||||
TrashIcon,
|
||||
UserIcon,
|
||||
SaveIcon,
|
||||
TransferIcon,
|
||||
UserPlusIcon,
|
||||
UserRemoveIcon,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
@ -430,6 +384,12 @@ export default {
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
await this.updateMembers()
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'Member(s) updated',
|
||||
text: `Your project's member(s) has been updated.`,
|
||||
type: 'success',
|
||||
})
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
@ -464,20 +424,6 @@ export default {
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
async deleteProject() {
|
||||
await this.$axios.delete(
|
||||
`project/${this.project.id}`,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
await this.$store.dispatch('user/fetchProjects')
|
||||
await this.$router.push(`/user/${this.$auth.user.username}`)
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'Action Success',
|
||||
text: 'Your project has been successfully deleted.',
|
||||
type: 'success',
|
||||
})
|
||||
},
|
||||
async updateMembers() {
|
||||
this.allTeamMembers = (
|
||||
await this.$axios.get(
|
||||
@ -518,11 +464,10 @@ export default {
|
||||
align-items: center;
|
||||
.dropdown-icon {
|
||||
margin-left: 1rem;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-dark);
|
||||
background-color: unset;
|
||||
transition: 150ms ease transform;
|
||||
padding: unset;
|
||||
|
||||
svg {
|
||||
transition: 150ms ease transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -546,7 +491,7 @@ export default {
|
||||
|
||||
&.open {
|
||||
.member-header {
|
||||
.dropdown-icon {
|
||||
.dropdown-icon svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
319
pages/_type/_id/settings/tags.vue
Normal file
@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Tags</span>
|
||||
</h3>
|
||||
</div>
|
||||
<p>
|
||||
Accurate tagging is important to help people find your
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }}. Make sure
|
||||
to select all tags that apply.
|
||||
</p>
|
||||
<template v-for="header in Object.keys(categoryLists)">
|
||||
<div :key="`categories-${header}`" class="label">
|
||||
<h4>
|
||||
<span class="label__title">{{
|
||||
$formatCategoryHeader(header)
|
||||
}}</span>
|
||||
</h4>
|
||||
<span class="label__description">
|
||||
<template v-if="header === 'categories'">
|
||||
Select all categories that reflect the themes or function of your
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }}.
|
||||
</template>
|
||||
<template v-else-if="header === 'features'">
|
||||
Select all of the features that your
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }} makes
|
||||
use of.
|
||||
</template>
|
||||
<template v-else-if="header === 'resolutions'">
|
||||
Select the resolution(s) of textures in your
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }}.
|
||||
</template>
|
||||
<template v-else-if="header === 'performance impact'">
|
||||
Select the realistic performance impact of your
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }}.
|
||||
Select multiple if the
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }} is
|
||||
configurable to different levels of performance impact.
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<div :key="`categories-${header}-list`" class="category-list input-div">
|
||||
<Checkbox
|
||||
v-for="category in categoryLists[header]"
|
||||
:key="`category-${header}-${category.name}`"
|
||||
:value="selectedTags.includes(category)"
|
||||
:description="$formatCategory(category.name)"
|
||||
class="category-selector"
|
||||
@input="toggleCategory(category)"
|
||||
>
|
||||
<div class="category-selector__label">
|
||||
<div
|
||||
v-if="header !== 'resolutions' && category.icon"
|
||||
aria-hidden="true"
|
||||
class="icon"
|
||||
v-html="category.icon"
|
||||
></div>
|
||||
<span aria-hidden="true">
|
||||
{{ $formatCategory(category.name) }}</span
|
||||
>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</template>
|
||||
<div class="label">
|
||||
<h4>
|
||||
<span class="label__title"><StarIcon /> Featured tags</span>
|
||||
</h4>
|
||||
<span class="label__description">
|
||||
You can feature up to 3 of your most relevant tags. Other tags may be
|
||||
promoted to featured if you do not select all 3.
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="selectedTags.length < 1">
|
||||
Select at least one category in order to feature a category.
|
||||
</p>
|
||||
<div class="category-list input-div">
|
||||
<Checkbox
|
||||
v-for="category in selectedTags"
|
||||
:key="`featured-category-${category.name}`"
|
||||
class="category-selector"
|
||||
:value="featuredTags.includes(category)"
|
||||
:description="$formatCategory(category.name)"
|
||||
:disabled="
|
||||
featuredTags.length >= 3 && !featuredTags.includes(category)
|
||||
"
|
||||
@input="toggleFeaturedCategory(category)"
|
||||
>
|
||||
<div class="category-selector__label">
|
||||
<div
|
||||
v-if="category.header !== 'resolutions' && category.icon"
|
||||
aria-hidden="true"
|
||||
class="icon"
|
||||
v-html="category.icon"
|
||||
></div>
|
||||
<span aria-hidden="true">
|
||||
{{ $formatCategory(category.name) }}</span
|
||||
>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button
|
||||
type="button"
|
||||
class="iconified-button brand-button"
|
||||
:disabled="!hasChanges"
|
||||
@click="saveChanges()"
|
||||
>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Checkbox from '~/components/ui/Checkbox'
|
||||
import StarIcon from '~/assets/images/utils/star.svg?inline'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Checkbox,
|
||||
SaveIcon,
|
||||
StarIcon,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
allMembers: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
return null
|
||||
},
|
||||
},
|
||||
patchProject: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'Patch project function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTags: [],
|
||||
featuredTags: [],
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.selectedTags = this.$sortedCategories.filter(
|
||||
(x) =>
|
||||
x.project_type === this.project.actualProjectType &&
|
||||
(this.project.categories.includes(x.name) ||
|
||||
this.project.additional_categories.includes(x.name))
|
||||
)
|
||||
this.featuredTags = this.$sortedCategories.filter(
|
||||
(x) =>
|
||||
x.project_type === this.project.actualProjectType &&
|
||||
this.project.categories.includes(x.name)
|
||||
)
|
||||
},
|
||||
computed: {
|
||||
categoryLists() {
|
||||
const lists = {}
|
||||
this.$sortedCategories.forEach((x) => {
|
||||
if (x.project_type === this.project.actualProjectType) {
|
||||
const header = x.header
|
||||
if (!lists[header]) {
|
||||
lists[header] = []
|
||||
}
|
||||
lists[header].push(x)
|
||||
}
|
||||
})
|
||||
return lists
|
||||
},
|
||||
patchData() {
|
||||
const data = {}
|
||||
// Promote selected categories to featured if there are less than 3 featured
|
||||
const newFeaturedTags = this.featuredTags.slice()
|
||||
if (
|
||||
newFeaturedTags.length < 1 &&
|
||||
this.selectedTags.length > newFeaturedTags.length
|
||||
) {
|
||||
const nonFeaturedCategories = this.selectedTags.filter(
|
||||
(x) => !newFeaturedTags.includes(x)
|
||||
)
|
||||
|
||||
nonFeaturedCategories
|
||||
.slice(
|
||||
0,
|
||||
Math.min(nonFeaturedCategories.length, 3 - newFeaturedTags.length)
|
||||
)
|
||||
.forEach((x) => newFeaturedTags.push(x))
|
||||
}
|
||||
// Convert selected and featured categories to backend-usable arrays
|
||||
const categories = newFeaturedTags.map((x) => x.name)
|
||||
const additionalCategories = this.selectedTags
|
||||
.filter((x) => !newFeaturedTags.includes(x))
|
||||
.map((x) => x.name)
|
||||
|
||||
if (
|
||||
categories.length !== this.project.categories.length ||
|
||||
categories.some((value) => !this.project.categories.includes(value))
|
||||
) {
|
||||
data.categories = categories
|
||||
}
|
||||
|
||||
if (
|
||||
additionalCategories.length !==
|
||||
this.project.additional_categories.length ||
|
||||
additionalCategories.some(
|
||||
(value) => !this.project.additional_categories.includes(value)
|
||||
)
|
||||
) {
|
||||
data.additional_categories = additionalCategories
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
hasChanges() {
|
||||
return Object.keys(this.patchData).length > 0
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleCategory(category) {
|
||||
if (this.selectedTags.includes(category)) {
|
||||
this.selectedTags = this.selectedTags.filter((x) => x !== category)
|
||||
if (this.featuredTags.includes(category)) {
|
||||
this.featuredTags = this.featuredTags.filter((x) => x !== category)
|
||||
}
|
||||
} else {
|
||||
this.selectedTags.push(category)
|
||||
}
|
||||
},
|
||||
toggleFeaturedCategory(category) {
|
||||
if (this.featuredTags.includes(category)) {
|
||||
this.featuredTags = this.featuredTags.filter((x) => x !== category)
|
||||
} else {
|
||||
this.featuredTags.push(category)
|
||||
}
|
||||
},
|
||||
saveChanges() {
|
||||
if (this.hasChanges) {
|
||||
this.patchProject(this.patchData)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.label__title {
|
||||
margin-top: var(--spacing-card-bg);
|
||||
|
||||
svg {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.category-list {
|
||||
column-count: 4;
|
||||
column-gap: var(--spacing-card-lg);
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
|
||||
.category-selector ::v-deep {
|
||||
margin-bottom: 0.5rem;
|
||||
.category-selector__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.icon {
|
||||
height: 1rem;
|
||||
svg {
|
||||
margin-right: 0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
span {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1250px) {
|
||||
column-count: 3;
|
||||
}
|
||||
@media only screen and (max-width: 1024px) {
|
||||
column-count: 4;
|
||||
}
|
||||
@media only screen and (max-width: 960px) {
|
||||
column-count: 3;
|
||||
}
|
||||
@media only screen and (max-width: 750px) {
|
||||
column-count: 2;
|
||||
}
|
||||
@media only screen and (max-width: 530px) {
|
||||
column-count: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -32,7 +32,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-for="version in filteredVersions"
|
||||
:key="version.id + '-new'"
|
||||
:key="version.id"
|
||||
class="version-button button-transparent"
|
||||
@click="
|
||||
$router.push(
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
<NavStackItem link="/dashboard" label="Overview">
|
||||
<DashboardIcon />
|
||||
</NavStackItem>
|
||||
<!-- <NavStackItem link="/dashboard/projects" label="Projects">-->
|
||||
<!-- <ListIcon />-->
|
||||
<!-- </NavStackItem>-->
|
||||
<NavStackItem link="/dashboard/projects" label="Projects">
|
||||
<ListIcon />
|
||||
</NavStackItem>
|
||||
<!-- <NavStackItem link="/dashboard/analytics" label="Analytics">-->
|
||||
<!-- <ChartIcon />-->
|
||||
<!-- </NavStackItem>-->
|
||||
@ -36,7 +36,7 @@ import NavStackItem from '~/components/ui/NavStackItem'
|
||||
import DashboardIcon from '~/assets/images/utils/dashboard.svg?inline'
|
||||
// import ChartIcon from '~/assets/images/utils/chart.svg?inline'
|
||||
import CurrencyIcon from '~/assets/images/utils/currency.svg?inline'
|
||||
// import ListIcon from '~/assets/images/utils/list.svg?inline'
|
||||
import ListIcon from '~/assets/images/utils/list.svg?inline'
|
||||
|
||||
const monetization = true
|
||||
|
||||
@ -48,7 +48,7 @@ export default {
|
||||
DashboardIcon,
|
||||
// ChartIcon,
|
||||
CurrencyIcon,
|
||||
// ListIcon,
|
||||
ListIcon,
|
||||
},
|
||||
methods: {
|
||||
hasMonetization() {
|
||||
|
||||
@ -2,6 +2,10 @@
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<h2>Analytics</h2>
|
||||
<p>You found a secret!</p>
|
||||
<nuxt-link to="/frog" class="goto-link"
|
||||
>Click here for fancy graphs!</nuxt-link
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<h2>Overview</h2>
|
||||
<div class="metrics">
|
||||
<div class="metric">
|
||||
<div class="grid-display">
|
||||
<div class="grid-display__item">
|
||||
<div class="label">Total downloads</div>
|
||||
<div class="value">
|
||||
{{
|
||||
@ -24,7 +24,7 @@
|
||||
<!-- aria-hidden="true"-->
|
||||
<!-- /></NuxtLink>-->
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="grid-display__item">
|
||||
<div class="label">Total followers</div>
|
||||
<div class="value">
|
||||
{{
|
||||
@ -47,7 +47,7 @@
|
||||
<!-- aria-hidden="true"-->
|
||||
<!-- /></NuxtLink>-->
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="grid-display__item">
|
||||
<div class="label">Total revenue</div>
|
||||
<div class="value">{{ $formatMoney(payouts.all_time) }}</div>
|
||||
<span>{{ $formatMoney(payouts.last_month) }} this month</span>
|
||||
@ -58,7 +58,7 @@
|
||||
<!-- aria-hidden="true"-->
|
||||
<!-- /></NuxtLink>-->
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="grid-display__item">
|
||||
<div class="label">Current balance</div>
|
||||
<div class="value">
|
||||
{{ $formatMoney($auth.user.payout_data.balance) }}
|
||||
@ -130,33 +130,4 @@ export default {
|
||||
methods: {},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.metrics {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-card-md);
|
||||
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background-color: var(--color-bg);
|
||||
border-radius: var(--size-rounded-card);
|
||||
padding: var(--spacing-card-lg);
|
||||
gap: var(--spacing-card-md);
|
||||
|
||||
.label {
|
||||
color: var(--color-heading);
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--color-text-dark);
|
||||
font-weight: bold;
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@ -1,22 +1,664 @@
|
||||
<template>
|
||||
<div>
|
||||
<Modal ref="editLinksModal" header="Edit links">
|
||||
<div class="universal-modal links-modal">
|
||||
<p>
|
||||
Any links you specify below will be overwritten on each of the
|
||||
selected projects. Any you leave blank will be ignored. You can clear
|
||||
a link from all selected projects using the trash can button.
|
||||
</p>
|
||||
<section class="links">
|
||||
<label
|
||||
for="issue-tracker-input"
|
||||
title="A place for users to report bugs, issues, and concerns about your project."
|
||||
>
|
||||
<span class="label__title">Issue tracker</span>
|
||||
</label>
|
||||
<div class="input-group shrink-first">
|
||||
<input
|
||||
id="issue-tracker-input"
|
||||
v-model="editLinks.issues.val"
|
||||
:disabled="editLinks.issues.clear"
|
||||
type="url"
|
||||
:placeholder="
|
||||
editLinks.issues.clear
|
||||
? 'Existing link will be cleared'
|
||||
: 'Enter a valid URL'
|
||||
"
|
||||
maxlength="2048"
|
||||
/>
|
||||
<button
|
||||
v-tooltip="'Clear link'"
|
||||
class="square-button label-button"
|
||||
:data-active="editLinks.issues.clear"
|
||||
@click="editLinks.issues.clear = !editLinks.issues.clear"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</div>
|
||||
<label
|
||||
for="source-code-input"
|
||||
title="A page/repository containing the source code for your project"
|
||||
>
|
||||
<span class="label__title">Source code</span>
|
||||
</label>
|
||||
<div class="input-group shrink-first">
|
||||
<input
|
||||
id="source-code-input"
|
||||
v-model="editLinks.source.val"
|
||||
:disabled="editLinks.source.clear"
|
||||
type="url"
|
||||
maxlength="2048"
|
||||
:placeholder="
|
||||
editLinks.source.clear
|
||||
? 'Existing link will be cleared'
|
||||
: 'Enter a valid URL'
|
||||
"
|
||||
/>
|
||||
<button
|
||||
v-tooltip="'Clear link'"
|
||||
class="square-button label-button"
|
||||
:data-active="editLinks.source.clear"
|
||||
@click="editLinks.source.clear = !editLinks.source.clear"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</div>
|
||||
<label
|
||||
for="wiki-page-input"
|
||||
title="A page containing information, documentation, and help for the project."
|
||||
>
|
||||
<span class="label__title">Wiki page</span>
|
||||
</label>
|
||||
<div class="input-group shrink-first">
|
||||
<input
|
||||
id="wiki-page-input"
|
||||
v-model="editLinks.wiki.val"
|
||||
:disabled="editLinks.wiki.clear"
|
||||
type="url"
|
||||
maxlength="2048"
|
||||
:placeholder="
|
||||
editLinks.wiki.clear
|
||||
? 'Existing link will be cleared'
|
||||
: 'Enter a valid URL'
|
||||
"
|
||||
/>
|
||||
<button
|
||||
v-tooltip="'Clear link'"
|
||||
class="square-button label-button"
|
||||
:data-active="editLinks.wiki.clear"
|
||||
@click="editLinks.wiki.clear = !editLinks.wiki.clear"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</div>
|
||||
<label
|
||||
for="discord-invite-input"
|
||||
title="An invitation link to your Discord server."
|
||||
>
|
||||
<span class="label__title">Discord invite</span>
|
||||
</label>
|
||||
<div class="input-group shrink-first">
|
||||
<input
|
||||
id="discord-invite-input"
|
||||
v-model="editLinks.discord.val"
|
||||
:disabled="editLinks.discord.clear"
|
||||
type="url"
|
||||
maxlength="2048"
|
||||
:placeholder="
|
||||
editLinks.discord.clear
|
||||
? 'Existing link will be cleared'
|
||||
: 'Enter a valid Discord invite URL'
|
||||
"
|
||||
/>
|
||||
<button
|
||||
v-tooltip="'Clear link'"
|
||||
class="square-button label-button"
|
||||
:data-active="editLinks.discord.clear"
|
||||
@click="editLinks.discord.clear = !editLinks.discord.clear"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<p>
|
||||
Changes will be applied to
|
||||
<strong>{{ selectedProjects.length }}</strong> project{{
|
||||
selectedProjects.length > 1 ? 's' : ''
|
||||
}}.
|
||||
</p>
|
||||
<ul>
|
||||
<li
|
||||
v-for="project in selectedProjects.slice(
|
||||
0,
|
||||
editLinks.showAffected ? selectedProjects.length : 3
|
||||
)"
|
||||
:key="project.id"
|
||||
>
|
||||
{{ project.title }}
|
||||
</li>
|
||||
<li v-if="!editLinks.showAffected && selectedProjects.length > 3">
|
||||
<strong>and {{ selectedProjects.length - 3 }} more...</strong>
|
||||
</li>
|
||||
</ul>
|
||||
<Checkbox
|
||||
v-if="selectedProjects.length > 3"
|
||||
v-model="editLinks.showAffected"
|
||||
:label="editLinks.showAffected ? 'Less' : 'More'"
|
||||
description="Show all loaders"
|
||||
:border="false"
|
||||
:collapsing-toggle-style="true"
|
||||
/>
|
||||
<div class="push-right input-group">
|
||||
<button class="iconified-button" @click="$refs.editLinksModal.hide()">
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
@click="bulkEditLinks()"
|
||||
>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<ModalCreation ref="modal_creation" />
|
||||
<section class="universal-card">
|
||||
<h2>Projects</h2>
|
||||
<div class="header__row">
|
||||
<h2 class="header__title">Projects</h2>
|
||||
<div class="input-group">
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
@click="$refs.modal_creation.show()"
|
||||
>
|
||||
<PlusIcon />
|
||||
Create a project
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="projects.length < 1">
|
||||
You don't have any projects yet. Click the green button above to begin.
|
||||
</p>
|
||||
<template v-else>
|
||||
<p>You can edit multiple projects at once by selecting them below.</p>
|
||||
<div class="input-group">
|
||||
<button
|
||||
class="iconified-button"
|
||||
:disabled="selectedProjects.length === 0"
|
||||
@click="$refs.editLinksModal.show()"
|
||||
>
|
||||
<EditIcon />
|
||||
Edit links
|
||||
</button>
|
||||
<div class="push-right">
|
||||
<div class="labeled-control-row">
|
||||
Sort By
|
||||
<Multiselect
|
||||
v-model="sortBy"
|
||||
:searchable="false"
|
||||
class="small-select"
|
||||
:options="['Name', 'Status', 'Type']"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
@input="updateSort()"
|
||||
></Multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-table">
|
||||
<div class="grid-table__row grid-table__header">
|
||||
<div>
|
||||
<Checkbox
|
||||
:value="selectedProjects === projects"
|
||||
@input="
|
||||
selectedProjects === projects
|
||||
? (selectedProjects = [])
|
||||
: (selectedProjects = projects)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div>Icon</div>
|
||||
<div>Name</div>
|
||||
<div>ID</div>
|
||||
<div>Type</div>
|
||||
<div>Status</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div
|
||||
v-for="project in projects"
|
||||
:key="`project-${project.id}`"
|
||||
class="grid-table__row"
|
||||
>
|
||||
<div>
|
||||
<Checkbox
|
||||
:disabled="
|
||||
(project.permissions & EDIT_DETAILS) === EDIT_DETAILS
|
||||
"
|
||||
:value="selectedProjects.includes(project)"
|
||||
@input="
|
||||
selectedProjects.includes(project)
|
||||
? (selectedProjects = selectedProjects.filter(
|
||||
(it) => it !== project
|
||||
))
|
||||
: selectedProjects.push(project)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<nuxt-link
|
||||
tabindex="-1"
|
||||
:to="`/${project.project_type}/${project.slug}`"
|
||||
>
|
||||
<Avatar
|
||||
:src="project.icon_url"
|
||||
aria-hidden="true"
|
||||
:alt="'Icon for ' + project.title"
|
||||
no-shadow
|
||||
/>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="project-title">
|
||||
<IssuesIcon
|
||||
v-if="project.moderator_message"
|
||||
v-tooltip="
|
||||
'Project has a message from the moderators. View the project to see more.'
|
||||
"
|
||||
aria-label="Project has a message from the moderators. View the project to see more."
|
||||
/>
|
||||
|
||||
<nuxt-link
|
||||
class="hover-link wrap-as-needed"
|
||||
:to="`/${project.project_type}/${project.slug}`"
|
||||
>
|
||||
{{ project.title }}
|
||||
</nuxt-link>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<CopyCode :text="project.id" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ $formatProjectType(project.project_type) }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Badge
|
||||
v-if="project.status"
|
||||
:type="project.status"
|
||||
class="status"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<nuxt-link
|
||||
class="square-button"
|
||||
:to="`/${project.project_type}/${project.slug}/settings`"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
|
||||
import Badge from '~/components/ui/Badge.vue'
|
||||
import Checkbox from '~/components/ui/Checkbox.vue'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
// import ModalConfirm from '~/components/ui/ModalConfirm.vue'
|
||||
import Avatar from '~/components/ui/Avatar.vue'
|
||||
import ModalCreation from '~/components/ui/ModalCreation.vue'
|
||||
import CopyCode from '~/components/ui/CopyCode.vue'
|
||||
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import IssuesIcon from '~/assets/images/utils/issues.svg?inline'
|
||||
import PlusIcon from '~/assets/images/utils/plus.svg?inline'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg?inline'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {}
|
||||
components: {
|
||||
Avatar,
|
||||
Badge,
|
||||
SettingsIcon,
|
||||
TrashIcon,
|
||||
Checkbox,
|
||||
IssuesIcon,
|
||||
PlusIcon,
|
||||
CrossIcon,
|
||||
EditIcon,
|
||||
SaveIcon,
|
||||
Modal,
|
||||
// ModalConfirm,
|
||||
ModalCreation,
|
||||
Multiselect,
|
||||
CopyCode,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
projects: [],
|
||||
versions: [],
|
||||
selectedProjects: [],
|
||||
sortBy: 'Name',
|
||||
editLinks: {
|
||||
showAffected: false,
|
||||
source: {
|
||||
val: '',
|
||||
clear: false,
|
||||
},
|
||||
discord: {
|
||||
val: '',
|
||||
clear: false,
|
||||
},
|
||||
wiki: {
|
||||
val: '',
|
||||
clear: false,
|
||||
},
|
||||
issues: {
|
||||
val: '',
|
||||
clear: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.projects = this.$user.projects
|
||||
this.updateSort()
|
||||
},
|
||||
fetch() {},
|
||||
head: {
|
||||
title: 'Projects - Modrinth',
|
||||
},
|
||||
methods: {},
|
||||
created() {
|
||||
this.UPLOAD_VERSION = 1 << 0
|
||||
this.DELETE_VERSION = 1 << 1
|
||||
this.EDIT_DETAILS = 1 << 2
|
||||
this.EDIT_BODY = 1 << 3
|
||||
this.MANAGE_INVITES = 1 << 4
|
||||
this.REMOVE_MEMBER = 1 << 5
|
||||
this.EDIT_MEMBER = 1 << 6
|
||||
this.DELETE_PROJECT = 1 << 7
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
updateSort() {
|
||||
switch (this.sortBy) {
|
||||
case 'Name':
|
||||
this.projects = this.projects.slice().sort((a, b) => {
|
||||
if (a.title < b.title) {
|
||||
return -1
|
||||
}
|
||||
if (a.title > b.title) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
break
|
||||
case 'Status':
|
||||
this.projects = this.projects.slice().sort((a, b) => {
|
||||
if (a.status < b.status) {
|
||||
return -1
|
||||
}
|
||||
if (a.status > b.status) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
break
|
||||
case 'Type':
|
||||
this.projects = this.projects.slice().sort((a, b) => {
|
||||
if (a.project_type < b.project_type) {
|
||||
return -1
|
||||
}
|
||||
if (a.project_type > b.project_type) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
async bulkEditLinks() {
|
||||
try {
|
||||
const baseData = {
|
||||
issues_url:
|
||||
!this.editLinks.issues.clear &&
|
||||
this.editLinks.issues.val.trim() !== ''
|
||||
? this.editLinks.issues.val
|
||||
: null,
|
||||
source_url:
|
||||
!this.editLinks.source.clear &&
|
||||
this.editLinks.source.val.trim() !== ''
|
||||
? this.editLinks.source.val
|
||||
: null,
|
||||
wiki_url:
|
||||
!this.editLinks.wiki.clear && this.editLinks.wiki.val.trim() !== ''
|
||||
? this.editLinks.wiki.val
|
||||
: null,
|
||||
discord_url:
|
||||
!this.editLinks.discord.clear &&
|
||||
this.editLinks.discord.val.trim() !== ''
|
||||
? this.editLinks.discord.val
|
||||
: null,
|
||||
}
|
||||
|
||||
await this.$axios.patch(
|
||||
`projects?ids=${JSON.stringify(
|
||||
this.selectedProjects.map((x) => x.id)
|
||||
)}`,
|
||||
baseData,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
|
||||
this.$refs.editLinksModal.hide()
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'Success',
|
||||
text: "Bulk edited selected project's links.",
|
||||
type: 'success',
|
||||
})
|
||||
this.selectedProjects = []
|
||||
} catch (e) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: e,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.grid-table {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
min-content min-content minmax(min-content, 2fr)
|
||||
minmax(min-content, 1fr) minmax(min-content, 1fr) minmax(min-content, 1fr) min-content;
|
||||
border-radius: var(--size-rounded-sm);
|
||||
overflow: hidden;
|
||||
margin-top: var(--spacing-card-md);
|
||||
|
||||
.grid-table__row {
|
||||
display: contents;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-card-sm);
|
||||
|
||||
// Left edge of table
|
||||
&:first-child {
|
||||
padding-left: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
// Right edge of table
|
||||
&:last-child {
|
||||
padding-right: var(--spacing-card-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2n + 1) > div {
|
||||
background-color: var(--color-table-alternate-row);
|
||||
}
|
||||
|
||||
&.grid-table__header > div {
|
||||
background-color: var(--color-bg);
|
||||
font-weight: bold;
|
||||
color: var(--color-text-dark);
|
||||
padding-top: var(--spacing-card-bg);
|
||||
padding-bottom: var(--spacing-card-bg);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.grid-table__row {
|
||||
display: grid;
|
||||
grid-template: 'checkbox icon name type settings' 'checkbox icon id status settings';
|
||||
grid-template-columns:
|
||||
min-content min-content minmax(min-content, 2fr)
|
||||
minmax(min-content, 1fr) min-content;
|
||||
|
||||
:nth-child(1) {
|
||||
grid-area: checkbox;
|
||||
}
|
||||
|
||||
:nth-child(2) {
|
||||
grid-area: icon;
|
||||
}
|
||||
|
||||
:nth-child(3) {
|
||||
grid-area: name;
|
||||
}
|
||||
|
||||
:nth-child(4) {
|
||||
grid-area: id;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
:nth-child(5) {
|
||||
grid-area: type;
|
||||
}
|
||||
|
||||
:nth-child(6) {
|
||||
grid-area: status;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
:nth-child(7) {
|
||||
grid-area: settings;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-table__header {
|
||||
grid-template: 'checkbox settings';
|
||||
grid-template-columns: min-content minmax(min-content, 1fr);
|
||||
|
||||
:nth-child(2),
|
||||
:nth-child(3),
|
||||
:nth-child(4),
|
||||
:nth-child(5),
|
||||
:nth-child(6) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 560px) {
|
||||
.grid-table__row {
|
||||
display: grid;
|
||||
grid-template: 'checkbox icon name settings' 'checkbox icon id settings' 'checkbox icon type settings' 'checkbox icon status settings';
|
||||
grid-template-columns: min-content min-content minmax(min-content, 1fr) min-content;
|
||||
|
||||
:nth-child(5) {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-table__header {
|
||||
grid-template: 'checkbox settings';
|
||||
grid-template-columns: min-content minmax(min-content, 1fr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.project-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-card-xs);
|
||||
|
||||
svg {
|
||||
color: var(--color-special-orange);
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: var(--spacing-card-xs);
|
||||
}
|
||||
|
||||
.hover-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.labeled-control-row {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
.small-select {
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.label-button[data-active='true'] {
|
||||
--background-color: var(--color-special-red);
|
||||
--text-color: var(--color-brand-inverted);
|
||||
}
|
||||
|
||||
.links-modal {
|
||||
.links {
|
||||
display: grid;
|
||||
gap: var(--spacing-card-sm);
|
||||
grid-template-columns: 1fr 2fr;
|
||||
|
||||
.input-group {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 530px) {
|
||||
grid-template-columns: 1fr;
|
||||
.input-group {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0 0 var(--spacing-card-sm) 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
>
|
||||
</p>
|
||||
|
||||
<div v-if="enrolled" class="buttons">
|
||||
<div v-if="enrolled" class="input-group">
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
@click="$refs.modal_transfer.show()"
|
||||
@ -122,10 +122,4 @@ strong {
|
||||
color: var(--color-text-dark);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-card-sm);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
<template>
|
||||
<div class="normal-page">
|
||||
<div class="card">
|
||||
<h1>Frog</h1>
|
||||
<p>You've been frogged! 🐸</p>
|
||||
<img
|
||||
src="https://cdn.modrinth.com/frog.png"
|
||||
alt="a photorealistic painting of a frog labyrinth"
|
||||
/>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h1>Frog</h1>
|
||||
<p>You've been frogged! 🐸</p>
|
||||
<img
|
||||
src="https://cdn.modrinth.com/frog.png"
|
||||
alt="a photorealistic painting of a frog labyrinth"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -19,11 +17,17 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
width: 100%;
|
||||
width: calc(100% - 2 * var(--spacing-card-md));
|
||||
max-width: 1280px;
|
||||
margin-inline: auto;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
margin-block: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
img {
|
||||
margin-block: 0 1.5rem;
|
||||
width: 60%;
|
||||
max-width: 40rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -91,17 +91,24 @@
|
||||
>
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="setProjectStatus(project, 'approved')"
|
||||
@click="
|
||||
setProjectStatus(
|
||||
project,
|
||||
project.requested_status
|
||||
? project.requested_status
|
||||
: 'approved'
|
||||
)
|
||||
"
|
||||
>
|
||||
<CheckIcon />
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="setProjectStatus(project, 'unlisted')"
|
||||
@click="setProjectStatus(project, 'withheld')"
|
||||
>
|
||||
<UnlistIcon />
|
||||
Unlist
|
||||
Withhold
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button"
|
||||
|
||||
@ -12,22 +12,23 @@
|
||||
:label="NOTIFICATION_TYPES[type]"
|
||||
>
|
||||
</NavStackItem>
|
||||
</NavStack>
|
||||
<h3>Manage</h3>
|
||||
<div class="input-group">
|
||||
<NuxtLink class="iconified-button" to="/settings/follows">
|
||||
<h3>Manage</h3>
|
||||
<NavStackItem
|
||||
link="/settings/follows"
|
||||
label="Followed projects"
|
||||
chevron
|
||||
>
|
||||
<SettingsIcon />
|
||||
Followed projects
|
||||
</NuxtLink>
|
||||
<button
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
v-if="$user.notifications.length > 0"
|
||||
class="iconified-button danger-button"
|
||||
@click="clearNotifications"
|
||||
:action="clearNotifications"
|
||||
label="Clear all"
|
||||
danger
|
||||
>
|
||||
<ClearIcon />
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
</NavStackItem>
|
||||
</NavStack>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
|
||||
@ -46,26 +46,9 @@
|
||||
</h3>
|
||||
|
||||
<SearchFilter
|
||||
v-for="category in categories
|
||||
.filter((x) => x.project_type === projectType.actual)
|
||||
.sort((a, b) => {
|
||||
if (header === 'resolutions') {
|
||||
return (
|
||||
a.name.replace(/\D/g, '') - b.name.replace(/\D/g, '')
|
||||
)
|
||||
} else if (header === 'performance impact') {
|
||||
const x = [
|
||||
'potato',
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
'screenshot',
|
||||
]
|
||||
|
||||
return x.indexOf(a.name) - x.indexOf(b.name)
|
||||
}
|
||||
return 0
|
||||
})"
|
||||
v-for="category in categories.filter(
|
||||
(x) => x.project_type === projectType.actual
|
||||
)"
|
||||
:key="category.name"
|
||||
:active-filters="facets"
|
||||
:display-name="$formatCategory(category.name)"
|
||||
@ -580,7 +563,7 @@ export default {
|
||||
categoriesMap() {
|
||||
const categories = {}
|
||||
|
||||
for (const category of this.$tag.categories) {
|
||||
for (const category of this.$sortedCategories) {
|
||||
if (categories[category.header]) {
|
||||
categories[category.header].push(category)
|
||||
} else {
|
||||
@ -588,16 +571,10 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
const newVals = Object.keys(categories)
|
||||
.sort()
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = categories[key]
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
for (const header of Object.keys(categories)) {
|
||||
newVals[header].sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
const newVals = Object.keys(categories).reduce((obj, key) => {
|
||||
obj[key] = categories[key]
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
return newVals
|
||||
},
|
||||
|
||||
@ -15,11 +15,7 @@
|
||||
<NavStackItem link="/settings/follows" label="Followed projects">
|
||||
<HeartIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
link="/settings/monetization"
|
||||
label="Monetization"
|
||||
beta
|
||||
>
|
||||
<NavStackItem link="/settings/monetization" label="Monetization">
|
||||
<CurrencyIcon />
|
||||
</NavStackItem>
|
||||
</template>
|
||||
|
||||
@ -64,7 +64,7 @@
|
||||
<div
|
||||
v-for="projectType in listTypes"
|
||||
:key="projectType.id + '-display-mode-selector'"
|
||||
class="adjacent-input small"
|
||||
class="adjacent-input"
|
||||
>
|
||||
<label :for="projectType.id + '-search-display-mode'">
|
||||
<span class="label__title">{{ projectType.name }} display mode</span>
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
Program. Setup a method of receiving payments below to enable
|
||||
monetization.
|
||||
</p>
|
||||
<div class="enroll extend-styling">
|
||||
<div class="enroll universal-body">
|
||||
<Chips
|
||||
v-model="selectedWallet"
|
||||
:starting-value="selectedWallet"
|
||||
|
||||
@ -156,7 +156,7 @@
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<Advertisement type="banner" small-screen="square" />
|
||||
<nav class="card user-navigation">
|
||||
<nav class="navigation-card">
|
||||
<NavRow
|
||||
query="type"
|
||||
:links="[
|
||||
@ -567,15 +567,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.user-navigation {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 0.5rem;
|
||||
padding-right: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
padding-top: 2.5rem;
|
||||
}
|
||||
|
||||
@ -31,11 +31,14 @@ export default (ctx, inject) => {
|
||||
inject('formatProjectType', formatProjectType)
|
||||
inject('formatCategory', formatCategory)
|
||||
inject('formatCategoryHeader', formatCategoryHeader)
|
||||
inject('formatProjectStatus', formatProjectStatus)
|
||||
inject('computeVersions', (versions) => {
|
||||
const visitedVersions = []
|
||||
const returnVersions = []
|
||||
|
||||
for (const version of versions.reverse()) {
|
||||
for (const version of versions.sort(
|
||||
(a, b) => ctx.$dayjs(a.date_published) - ctx.$dayjs(b.date_published)
|
||||
)) {
|
||||
if (visitedVersions.includes(version.version_number)) {
|
||||
visitedVersions.push(version.version_number)
|
||||
version.displayUrlEnding = version.id
|
||||
@ -47,18 +50,23 @@ export default (ctx, inject) => {
|
||||
returnVersions.push(version)
|
||||
}
|
||||
|
||||
return returnVersions.reverse().map((version, index) => {
|
||||
const nextVersion = returnVersions[index + 1]
|
||||
if (
|
||||
nextVersion &&
|
||||
version.changelog &&
|
||||
nextVersion.changelog === version.changelog
|
||||
) {
|
||||
return { duplicate: true, ...version }
|
||||
} else {
|
||||
return { duplicate: false, ...version }
|
||||
}
|
||||
})
|
||||
return returnVersions
|
||||
.reverse()
|
||||
.map((version, index) => {
|
||||
const nextVersion = returnVersions[index + 1]
|
||||
if (
|
||||
nextVersion &&
|
||||
version.changelog &&
|
||||
nextVersion.changelog === version.changelog
|
||||
) {
|
||||
return { duplicate: true, ...version }
|
||||
} else {
|
||||
return { duplicate: false, ...version }
|
||||
}
|
||||
})
|
||||
.sort(
|
||||
(a, b) => ctx.$dayjs(b.date_published) - ctx.$dayjs(a.date_published)
|
||||
)
|
||||
})
|
||||
inject('getProjectTypeForDisplay', (type, categories) => {
|
||||
if (type === 'mod') {
|
||||
@ -115,6 +123,26 @@ export default (ctx, inject) => {
|
||||
}
|
||||
})
|
||||
inject('cycleValue', cycleValue)
|
||||
const sortedCategories = ctx.store.state.tag.categories
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const headerCompare = a.header.localeCompare(b.header)
|
||||
if (headerCompare !== 0) {
|
||||
return headerCompare
|
||||
}
|
||||
if (a.header === 'resolutions' && b.header === 'resolutions') {
|
||||
return a.name.replace(/\D/g, '') - b.name.replace(/\D/g, '')
|
||||
} else if (
|
||||
a.header === 'performance impact' &&
|
||||
b.header === 'performance impact'
|
||||
) {
|
||||
const x = ['potato', 'low', 'medium', 'high', 'screenshot']
|
||||
|
||||
return x.indexOf(a.name) - x.indexOf(b.name)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
inject('sortedCategories', sortedCategories)
|
||||
}
|
||||
|
||||
export const formatNumber = (number) => {
|
||||
@ -158,7 +186,7 @@ export const formatBytes = (bytes, decimals = 2) => {
|
||||
}
|
||||
|
||||
export const capitalizeString = (name) => {
|
||||
return name.charAt(0).toUpperCase() + name.slice(1)
|
||||
return name ? name.charAt(0).toUpperCase() + name.slice(1) : name
|
||||
}
|
||||
|
||||
export const formatWallet = (name) => {
|
||||
@ -205,6 +233,10 @@ export const formatCategory = (name) => {
|
||||
return 'PBR'
|
||||
} else if (name === 'datapack') {
|
||||
return 'Data Pack'
|
||||
} else if (name === 'colored-lighting') {
|
||||
return 'Colored Lighting'
|
||||
} else if (name === 'optifine') {
|
||||
return 'OptiFine'
|
||||
}
|
||||
|
||||
return capitalizeString(name)
|
||||
@ -214,6 +246,16 @@ export const formatCategoryHeader = (name) => {
|
||||
return capitalizeString(name)
|
||||
}
|
||||
|
||||
export const formatProjectStatus = (name) => {
|
||||
if (name === 'approved') {
|
||||
return 'Listed'
|
||||
} else if (name === 'processing') {
|
||||
return 'Under review'
|
||||
}
|
||||
|
||||
return capitalizeString(name)
|
||||
}
|
||||
|
||||
export const formatVersions = (versionArray, store) => {
|
||||
const allVersions = store.state.tag.gameVersions.slice().reverse()
|
||||
const allReleases = allVersions.filter((x) => x.version_type === 'release')
|
||||
|
||||
@ -89,14 +89,14 @@ export const actions = {
|
||||
async fetchProjects({ commit, rootState }) {
|
||||
if (rootState.auth.user && rootState.auth.user.id) {
|
||||
try {
|
||||
const follows = (
|
||||
const projects = (
|
||||
await this.$axios.get(
|
||||
`user/${rootState.auth.user.id}/follows`,
|
||||
`user/${rootState.auth.user.id}/projects`,
|
||||
rootState.auth.headers
|
||||
)
|
||||
).data
|
||||
|
||||
commit('SET_FOLLOWS', follows)
|
||||
commit('SET_PROJECTS', projects)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||