Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Various perks for donators of the WiiLink24 service #1

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
82d8cdd
feat: Add donor features
matthe815 Aug 1, 2024
ac46096
feat: Add donor tags
matthe815 Aug 1, 2024
ed66083
feat: Regen tag on donor
matthe815 Aug 1, 2024
d78bf15
feat: Add selectable donor colors
matthe815 Aug 1, 2024
d543b59
sql: Add donor migrations
matthe815 Aug 1, 2024
91cf46e
feat: Add color selection
matthe815 Aug 1, 2024
07cd0bf
feat: Add custom color picker
matthe815 Aug 1, 2024
d8c9772
fix: Set default color to actual color
matthe815 Aug 1, 2024
4f5dddb
feat: Custom backgrounds for donors
matthe815 Aug 1, 2024
f3bf22d
refactor: Conform custom background to RESTFUL standard
matthe815 Aug 1, 2024
07235e1
cleanup: Remove old unneeded background file
matthe815 Aug 1, 2024
b0ff26f
refactor: Include sanity check for name color
matthe815 Aug 1, 2024
9c60edb
Update lint.yml
matthe815 Aug 2, 2024
0001571
refactor: Fix linting
matthe815 Aug 2, 2024
ae6d576
Merge branch 'feature/donors' of https://github.com/WiiLink24/LinkTag…
matthe815 Aug 2, 2024
5e90f2c
fix: Show "custom" when using a custom background
matthe815 Aug 2, 2024
2129556
fix: Make donor update toast better
matthe815 Aug 2, 2024
daf75da
refactor: Move css style overrides to CSS class
matthe815 Aug 2, 2024
1025372
refactor: Change user more descriptive
matthe815 Aug 2, 2024
f06e601
refactor: Fix background naming to be consistent
matthe815 Aug 2, 2024
d0e5c62
refactor: Make user more descriptive
matthe815 Aug 2, 2024
b352af8
refactor: Remove unused imports
matthe815 Aug 2, 2024
3b909b4
i18n: Add missing keys to other two languages
matthe815 Aug 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: "16"
node-version: "20"

- name: Install dependencies
run: npm ci
Expand Down
3 changes: 3 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"hidden": "Hidden",
"administrator": "Administrator",
"moderator": "Moderator",
"donor": "Donator",
"play_log": "Play Log",

"registered_on": "<strong>Registered</strong>",
Expand All @@ -43,6 +44,8 @@
"select_font": "Select Font",

"images": "Images",
"donators": "Donator Benefits",
"name_color": "Name Colors",

"home": "Home",
"profile": "Profile",
Expand Down
3 changes: 3 additions & 0 deletions prisma/migrations/20240801065036_donors/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "user" ADD COLUMN "isDonor" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "user" ADD COLUMN "nameColor" TEXT NOT NULL DEFAULT '#000000';
4 changes: 3 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,12 @@ model user {
created_at DateTime @default(dbgenerated("CURRENT_TIMESTAMP(3)"))
updated_at DateTime @default(dbgenerated("CURRENT_TIMESTAMP(3)"))
badge String? @db.VarChar(50)
language String @default("en") @db.VarChar(11)
isBanned Int @default(0) @db.SmallInt
isPublic Int @default(1) @db.SmallInt
publicOverride Int? @db.SmallInt
language String @default("en") @db.VarChar(11)
isDonor Boolean @default(false)
nameColor String @default("#000000")
accounts accounts[]
banned_user banned_user[]
game_sessions game_sessions[]
Expand Down
42 changes: 42 additions & 0 deletions src/components/account/DonorButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { React } from 'react'
import { useRouter } from 'next/router'
import { toast } from 'react-toastify'
import useInfo from '@/lib/swr-hooks/useInfo'
import { Button } from 'react-bootstrap'
import PropTypes from 'prop-types'

export default function DonorButton ({ isDonor, id }) {
const router = useRouter()
const { mutate } = useInfo()

const setDonor = async (status) => {
const response = await fetch('/api/account/donor', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
status,
user: id
})
})
if (response.status === 200) {
toast.success('The account has been set to donor.')
matthe815 marked this conversation as resolved.
Show resolved Hide resolved
mutate()
router.reload()
} else {
toast.error('An error occured, please try again later.')
}
}

return (
<Button variant={isDonor ? 'danger' : 'primary'} onClick={() => setDonor(!isDonor)}>
{isDonor ? 'Remove Donor Status' : 'Set Donor'}
</Button>
)
}

DonorButton.propTypes = {
id: PropTypes.number.isRequired,
isDonor: PropTypes.bool.isRequired
}
79 changes: 79 additions & 0 deletions src/components/edit/DonorsCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import { Alert, Card, Col, Row } from 'react-bootstrap'
import LocalizedString from '../shared/LocalizedString'
import LanguageContext from '../shared/LanguageContext'

function ColorBox ({ color, onClick, current }) {
return (
<div id={color} style={{ backgroundColor: color, width: '48px', height: '48px', marginLeft: '8px', marginTop: '8px', boxShadow: current === color ? '0px 0px 7px 2px #0099FF' : null, borderRadius: '8px' }} onClick={(e) => onClick(e, color)} />
matthe815 marked this conversation as resolved.
Show resolved Hide resolved
)
}

function ColorPicker ({ current, setColor }) {
const [color, setColorState] = useState(current)

return (
<div id={current} style={{ width: '48px', height: '48px', marginLeft: '8px', marginTop: '8px', boxShadow: current === color ? '0px 0px 7px 2px #0099FF' : null }}>
<input id={current} type='color' style={{ height: '100%' }} value={current} onChange={(e) => { setColorState(e.target.value); setColor(e, e.target.value) }} />
<p style={{ fontSize: '10px', textAlign: 'center', width: '48px', color: '#DDD' }}>Custom</p>
matthe815 marked this conversation as resolved.
Show resolved Hide resolved
</div>
)
}

ColorPicker.propTypes = {
current: PropTypes.string.isRequired,
setColor: PropTypes.func.isRequired
}

ColorBox.propTypes = {
color: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
current: PropTypes.string.isRequired
}

function DonorsCard ({ values, errors, handleChange }) {
const setColor = (e, color) => {
values.nameColor = color
handleChange(e)
}

return (
<LanguageContext.Helper.Consumer>
{(lang) => (
<Card className='mb-3' bg='secondary' text='white'>
<Card.Header as='h5'><LocalizedString string='donators' /></Card.Header>
<Card.Body>
<Row className='mb-3'>
<Col md={5}>
<p><LocalizedString string='name_color' /></p>
<div className='colorboxes' style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap' }}>
matthe815 marked this conversation as resolved.
Show resolved Hide resolved
<ColorBox current={values.nameColor} color={'#000000'} onClick={setColor} />
<ColorBox current={values.nameColor} color={'#14d314'} onClick={setColor} />
<ColorBox current={values.nameColor} color={'#FFD700'} onClick={setColor} />
<ColorBox current={values.nameColor} color={'#0959de'} onClick={setColor} />
<ColorBox current={values.nameColor} color={'#ff3515'} onClick={setColor} />
<ColorBox current={values.nameColor} color={'#a81bc4'} onClick={setColor} />
<ColorPicker current={values.nameColor} setColor={setColor} />
</div>
{errors.overlay && (
<Alert className='mt-2 p-2' variant='danger'>
{errors.overlay}
</Alert>
)}
</Col>
</Row>
</Card.Body>
</Card>
)}
</LanguageContext.Helper.Consumer>
)
}

DonorsCard.propTypes = {
values: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
handleChange: PropTypes.func.isRequired
}

export default DonorsCard
30 changes: 27 additions & 3 deletions src/components/edit/ImagesCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const backgrounds = BACKGROUNDS.map((background) => ({
label: background
}))

function ImagesCard ({ values, errors, handleChange }) {
function ImagesCard ({ values, errors, handleChange, user }) {
matthe815 marked this conversation as resolved.
Show resolved Hide resolved
return (
<LanguageContext.Helper.Consumer>
{(lang) => (
Expand Down Expand Up @@ -66,12 +66,35 @@ function ImagesCard ({ values, errors, handleChange }) {
{errors.background}
</Alert>
)}
<hr />
<Form.Control
id="fileInput"
accept=".png"
name="file"
type="file"
onChange={(event) => {
const formData = new FormData()
formData.append('file', event.currentTarget.files[0])

values.background = `${user}.png`

return fetch('/api/account/background', {
method: 'POST',
body: formData
})
}}
/>
<p>
<small className="text-muted">
Please ensure that your image is 1200x450 and is in PNG format.
</small>
</p>
</Col>
<Col md={7}>
<img
alt='Background Preview'
className='img-thumbnail mx-auto d-block'
src={`/img/background/${values.background}`}
src={!Number.isNaN(Number(values.background.replace(/.*\//, '').replace(/\.png$/, ''))) ? 'api/account/background' : `/img/background/${values.background}`}
/>
</Col>
</Row>
Expand Down Expand Up @@ -134,7 +157,8 @@ function ImagesCard ({ values, errors, handleChange }) {
ImagesCard.propTypes = {
values: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
handleChange: PropTypes.func.isRequired
handleChange: PropTypes.func.isRequired,
user: PropTypes.string.isRequired
}

export default ImagesCard
2 changes: 1 addition & 1 deletion src/components/shared/AppNavbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function AppNavbar () {
<img
alt="LinkTag Logo"
className="d-inline-block align-text-top no-shadow"
height={46}
height={36}
src="/logo.png"
width={128}
/>
Expand Down
3 changes: 3 additions & 0 deletions src/components/user/UserInformationCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import BanAccountButton from '../account/BanAccountButton'
import ForceHiddenAccountButton from '../account/ForceHiddenAccountButton'
import LanguageContext from '../shared/LanguageContext'
import LocalizedString from '../shared/LocalizedString'
import DonorButton from '@/components/account/DonorButton'

function UserInformationCard ({ user, isLoggedIn, isAdmin, isMod }) {
return (
Expand All @@ -29,6 +30,7 @@ function UserInformationCard ({ user, isLoggedIn, isAdmin, isMod }) {
{(user.publicOverride === 0 || (user.publicOverride === 1 && isMod)) && (<Badge bg='danger' className='mb-2'><LocalizedString string='hidden'/></Badge>)}
{user.role === 'admin' && (<Badge bg='success' className='mb-2'><LocalizedString string='administrator'/></Badge>)}
{user.role === 'mod' && (<Badge bg='success' className='mb-2'><LocalizedString string='moderator'/></Badge>)}
{user.isDonor === true && (<Badge bg='primary' className='mb-2'><LocalizedString string='donor'/></Badge>)}
<ul className='list-unstyled m-0'>
<li>
<LocalizedString string='display_name'/>: {user.display_name}
Expand Down Expand Up @@ -71,6 +73,7 @@ function UserInformationCard ({ user, isLoggedIn, isAdmin, isMod }) {
<InputGroup>
<BanAccountButton isBanned={user.isBanned} id={user.id} />
<ForceHiddenAccountButton isHidden={user.publicOverride != null} id={user.id} />
<DonorButton isDonor={user.isDonor} id={user.id} />
</InputGroup>
</div>
)}
Expand Down
3 changes: 2 additions & 1 deletion src/lib/constants/filePaths.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export const CACHE = Object.freeze({
COVER: path.resolve(CACHE_PATH, 'cover'),
MIIS: path.resolve(CACHE_PATH, 'mii', 'user'),
TAGS: path.resolve(CACHE_PATH, 'tags'),
WADS: path.resolve(CACHE_PATH, 'wads')
WADS: path.resolve(CACHE_PATH, 'wads'),
BACKGROUNDS: path.resolve(CACHE_PATH, 'backgrounds')
})

export const PUBLIC = Object.freeze({
Expand Down
4 changes: 2 additions & 2 deletions src/lib/riitag/neo/std/Background.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PUBLIC } from '@/lib/constants/filePaths'
import { PUBLIC, CACHE } from '@/lib/constants/filePaths'
import path from 'node:path'
import Canvas from 'canvas'
import fs from 'node:fs'
Expand All @@ -7,7 +7,7 @@ import logger from '@/lib/logger'

export default class Background extends ModuleBase {
async render (ctx: Canvas.CanvasRenderingContext2D, user): Promise<void> {
const bgPath = path.resolve(PUBLIC.BACKGROUND, user.background)
const bgPath = path.resolve(!Number.isNaN(Number(user.background.replace(/.*\//, '').replace(/\.png$/, ''))) ? CACHE.BACKGROUNDS : PUBLIC.BACKGROUND, user.background)

if (!fs.existsSync(bgPath)) {
logger.error(`Background image does not exist: ${bgPath}`)
Expand Down
1 change: 1 addition & 0 deletions src/lib/riitag/neo/std/Username.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default class Username extends ModuleBase {
logger.info(`User Font: ${user.font}`)
logger.info(`Font Info: ${this.font.name} ${this.font.size} ${this.font.style} ${this.font.color} ${this.font.force}`)

if (user.isDonor === true) this.font.color = user.nameColor
drawText(ctx, this.font, user.display_name, this.x, this.y, this.align)
}
}
19 changes: 17 additions & 2 deletions src/lib/utils/fileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import path from 'node:path'
import logger from '@/lib/logger'
import { Readable } from 'stream'
import { finished } from 'node:stream/promises'
import { Buffer } from 'buffer'

export const exists = async (filename) =>
export const exists = async (filename: string) =>
!!(await fs.promises.stat(filename).catch(() => null))

export async function saveFile (filepath, file: any | null) {
export async function saveFile (filepath: string, file: any | null) {
if (file == null) return

if (!(await exists(filepath))) {
Expand All @@ -20,3 +21,17 @@ export async function saveFile (filepath, file: any | null) {

logger.info('File saved successfully')
}

export async function saveFileBuffer (filepath: string, file: Buffer) {
logger.info(`Saving file to ${filepath}`)
if (!(await exists(filepath))) {
await fs.promises.mkdir(path.dirname(filepath), { recursive: true })
}

try {
await fs.promises.writeFile(filepath, file)
logger.info('File saved successfully')
} catch (error) {
logger.error('Error saving the file:', error)
}
}
Loading
Loading