Skip to content

Commit

Permalink
chore(vscode): Set ESLint as default formatter for TypeScript React f…
Browse files Browse the repository at this point in the history
…ilesAdd CORS headers for API routes in Next.js configfeat: Implement ModListItem and ProfileListItem componentsfix(api): Correct async function declarations for API routesfeat(api): Add middleware for logging API path accessfeat: Update mods page layout with search functionality and user profile interactionstyle: Adjust formatting and layout in tools page for better readability
  • Loading branch information
AlbinoGeek committed Feb 19, 2024
1 parent f4ca40a commit 07ab9ca
Show file tree
Hide file tree
Showing 9 changed files with 1,174 additions and 738 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,7 @@
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[typescriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
}
16 changes: 15 additions & 1 deletion next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,21 @@ const nextConfig = {
pathname: '**',
}
]
}
},
async headers() {
return [
{
// matching all API routes
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Credentials', value: 'true' },
{ key: 'Access-Control-Allow-Origin', value: '*' },
{ key: 'Access-Control-Allow-Methods', value: 'GET,OPTIONS,PATCH,DELETE,POST,PUT' },
{ key: 'Access-Control-Allow-Headers', value: 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version' },
]
}
]
},
}

export default nextConfig
146 changes: 146 additions & 0 deletions src/components/mods/ModListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/* eslint-disable @next/next/no-img-element */
import TrashIcon from '@mui/icons-material/Delete'
import DotsIcon from '@mui/icons-material/MoreVert'
import WorldIcon from '@mui/icons-material/Public'
import SettingsIcon from '@mui/icons-material/Settings'
import VerifiedIcon from '@mui/icons-material/VerifiedOutlined'
import Box from '@mui/material/Box'
import Divider from '@mui/material/Divider'
import IconButton from '@mui/material/IconButton'
import ListItemButton from '@mui/material/ListItemButton'
import Menu from '@mui/material/Menu'
import MenuItem from '@mui/material/MenuItem'
import Typography from '@mui/material/Typography'
import { useState } from 'react'

type Props = {
id: string
name: string
owner: string
summary: string
verified?: boolean
}

export default function ModListItem(props: Props): JSX.Element {
const {
id,
name,
owner,
summary,
verified,
} = props

const [menuAnchor, setMenuAnchor] = useState<null | HTMLElement>(null)
const menuOpen = Boolean(menuAnchor)

return <ListItemButton>
<img
alt={`Mod icon for ${name}`}
src={`https://picsum.photos/seed/${id}/96/96`}
style={{
height: 96,
width: 96,
}}
/>

<Box
sx={{
display: 'flex',
flex: 1,
flexDirection: 'column',
mt: -1,
}}
>
<Box
sx={{
alignItems: 'baseline',
display: 'flex',
flexDirection: 'row',
gap: 1,
mb: -0.75,
width: 'auto',
letterSpacing: '.15em',
wordSpacing: '-.35em',
}}
>
<Typography variant="h6">
{name.replaceAll('_', ' ').replaceAll('-', ' ')}
</Typography>

{verified && <VerifiedIcon
color="success"
fontSize="inherit"
sx={{
ml: -0.5,
position: 'relative',
top: 4,
}}
/>}

<Typography variant="subtitle2">
{owner}
</Typography>
</Box>

<Typography
sx={{
color: 'primary.dark',
fontSize: '90%',
lineHeight: 1,
maxHeight: 56,
letterSpacing: '.15em',
wordSpacing: '-.25em',
}}
variant="body2"
>
{summary}
</Typography>
</Box>

<IconButton
color="primary"
onClick={(e) => setMenuAnchor(e.currentTarget)}
>
<DotsIcon color="inherit" />
</IconButton>

<Menu
anchorEl={menuAnchor}
onClose={() => setMenuAnchor(null)}
open={menuOpen}
>
<MenuItem>
<WorldIcon
color="inherit"
fontSize="inherit"
sx={{ mr: 1 }}
/>
<Typography variant="body2">
Website
</Typography>
</MenuItem>
<Divider />
<MenuItem>
<SettingsIcon
color="inherit"
fontSize="inherit"
sx={{ mr: 1 }}
/>
<Typography variant="body2">
Settings
</Typography>
</MenuItem>
<Divider />
<MenuItem sx={{ color: 'error.light' }}>
<TrashIcon
color="inherit"
fontSize="inherit"
sx={{ mr: 1 }}
/>
<Typography variant="body2">
Uninstall
</Typography>
</MenuItem>
</Menu>
</ListItemButton>
}
102 changes: 102 additions & 0 deletions src/components/mods/ProfileListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/* eslint-disable @next/next/no-img-element */
import TrashIcon from '@mui/icons-material/Delete'
import DotsIcon from '@mui/icons-material/MoreVert'
import Box from '@mui/material/Box'
import IconButton from '@mui/material/IconButton'
import ListItemButton from '@mui/material/ListItemButton'
import Menu from '@mui/material/Menu'
import MenuItem from '@mui/material/MenuItem'
import Typography from '@mui/material/Typography'
import { useCallback, useState, type Dispatch, type MouseEvent, type SetStateAction } from 'react'
import { type Profile } from 'types/Profile'

type Props = {
onSelect: Dispatch<SetStateAction<Profile>>
profile: Profile
}

export default function ProfileListItem(props: Props): JSX.Element {
const {
onSelect,
profile,
} = props

const handleClick = useCallback(() => onSelect(profile), [onSelect, profile])

const [menuAnchor, setMenuAnchor] = useState<null | HTMLElement>(null)
const menuOpen = Boolean(menuAnchor)

const handleMenuClick = useCallback((event: MouseEvent<HTMLButtonElement>) => {
setMenuAnchor(event.currentTarget)
}, [])

const handleMenuClose = useCallback(() => {
setMenuAnchor(null)
}, [])

return <ListItemButton
selected={profile.id === '30'}
onClick={handleClick}
>
<img
alt={`Profile icon for ${profile.name}`}
src={`https://picsum.photos/seed/${profile.id}/40/40`}
style={{
height: 40,
width: 40,
}}
/>

<Box
sx={{
display: 'flex',
flex: 1,
flexDirection: 'column',
mx: 2,
}}
>
<Typography
sx={{
fontSize: `${24 * (1 - (profile.name.length / 75))}px !important`,
lineHeight: 1,
}}
variant='h6'
>
{profile.name}
</Typography>

{profile.owner && <Typography
color="text.secondary"
sx={{
fontSize: `${18 * (1 - (profile.owner.length / 75))}px !important`,
lineHeight: 1,
}}
variant='body2'
>
{profile.owner}
</Typography>}
</Box>

<IconButton
color="primary"
onClick={handleMenuClick}
size="small"
sx={{ mr: -1 }}
>
<DotsIcon color="inherit" fontSize="inherit" />
</IconButton>

<Menu
anchorEl={menuAnchor}
onClose={handleMenuClose}
open={menuOpen}
>
<MenuItem sx={{ color: 'error.light' }}>
<TrashIcon color="inherit" fontSize="inherit" sx={{ mr: 1 }} />
<Typography variant="body2">
Delete
</Typography>
</MenuItem>
</Menu>
</ListItemButton>
}
4 changes: 2 additions & 2 deletions src/pages/api/concrete/bug-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ const bugReportTemplate = `
`

// NextJS API Route for submitting bug reports
export default void async function ConcreteBugReport(
export default async function ConcreteBugReport(
req: BugReportRequest,
res: NextApiResponse
) {
): Promise<void> {
try {
await limiter.check(res, 10, 'CACHE_TOKEN') // 10 requests per minute
} catch {
Expand Down
11 changes: 11 additions & 0 deletions src/pages/api/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { NextRequest, NextResponse } from 'next/server'

export async function middleware(req: NextRequest): Promise<unknown> {
console.log('middleware.ts', req.nextUrl.pathname)

return NextResponse.next()
}

export const config = {
matcher: ['/api/:path*']
}
43 changes: 43 additions & 0 deletions src/pages/api/ts/package/[...id].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { type NextApiRequest, type NextApiResponse } from 'next/types'
import { ofetch } from 'ofetch'

// https://thunderstore.io/api/experimental/package/
export default async function TSExperimentalPackage(
req: NextApiRequest,
res: NextApiResponse
): Promise<void> {
const { id } = req.query

if (req.method === 'OPTIONS') {
return res.status(204).json({ status: 'ok' })
}

if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method Not Allowed' })
}

if (!Array.isArray(id) || id.length != 2) {
return res.status(400).json({ error: 'Invalid ID' })
}

const response = await ofetch(
`https://thunderstore.io/api/experimental/package/${id.join('/')}/`,
{
cache: 'no-store',
credentials: 'omit',
headers: {
'Accept': 'application/json',
'User-Agent': 'LethalModding/lethal-modding',
},
method: 'GET',
referrerPolicy: 'no-referrer',
redirect: 'follow',
}
)

if (!response) {
return res.status(500).json({ error: 'Failed to fetch data from Thunderstore' })
}

res.status(200).json(response)
}
Loading

0 comments on commit 07ab9ca

Please sign in to comment.