@ -1,3 +0,0 @@
|
|||||||
node_modules
|
|
||||||
.next
|
|
||||||
.turbo
|
|
3
.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||||
|
}
|
@ -1,21 +1,21 @@
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
name: scoresaber-reloaded-frontend
|
name: scoresaber-reloaded
|
||||||
namespace: public-services
|
namespace: public-services
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: scoresaber-reloaded-frontend
|
app: scoresaber-reloaded
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: scoresaber-reloaded-frontend
|
app: scoresaber-reloaded
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: scoresaber-reloaded-frontend-container
|
- name: scoresaber-reloaded-container
|
||||||
image: git.fascinated.cc/fascinated/scoresaber-reloaded-frontend:latest
|
image: git.fascinated.cc/fascinated/scoresaber-reloaded:latest
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 3000
|
- containerPort: 3000
|
@ -2,7 +2,7 @@
|
|||||||
apiVersion: traefik.io/v1alpha1
|
apiVersion: traefik.io/v1alpha1
|
||||||
kind: IngressRoute
|
kind: IngressRoute
|
||||||
metadata:
|
metadata:
|
||||||
name: scoresaber-reloaded-frontend-ingress
|
name: scoresaber-reloaded-ingress
|
||||||
namespace: public-services
|
namespace: public-services
|
||||||
annotations:
|
annotations:
|
||||||
kubernetes.io/ingress.class: traefik-external
|
kubernetes.io/ingress.class: traefik-external
|
||||||
@ -18,7 +18,7 @@ spec:
|
|||||||
- name: compress
|
- name: compress
|
||||||
namespace: traefik
|
namespace: traefik
|
||||||
services:
|
services:
|
||||||
- name: scoresaber-reloaded-frontend-service
|
- name: scoresaber-reloaded-service
|
||||||
port: 3000
|
port: 3000
|
||||||
tls:
|
tls:
|
||||||
secretName: fascinated-cc
|
secretName: fascinated-cc
|
@ -3,7 +3,7 @@ apiVersion: bitnami.com/v1alpha1
|
|||||||
kind: SealedSecret
|
kind: SealedSecret
|
||||||
metadata:
|
metadata:
|
||||||
creationTimestamp: null
|
creationTimestamp: null
|
||||||
name: ssr-frontend-secret
|
name: ssr-secret
|
||||||
namespace: public-services
|
namespace: public-services
|
||||||
spec:
|
spec:
|
||||||
encryptedData:
|
encryptedData:
|
||||||
@ -15,6 +15,6 @@ spec:
|
|||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
creationTimestamp: null
|
creationTimestamp: null
|
||||||
name: ssr-frontend-secret
|
name: ssr-secret
|
||||||
namespace: public-services
|
namespace: public-services
|
||||||
type: Opaque
|
type: Opaque
|
@ -2,7 +2,7 @@
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: scoresaber-reloaded-frontend-service
|
name: scoresaber-reloaded-service
|
||||||
namespace: public-services
|
namespace: public-services
|
||||||
spec:
|
spec:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
@ -10,4 +10,4 @@ spec:
|
|||||||
- port: 3000
|
- port: 3000
|
||||||
targetPort: 3000
|
targetPort: 3000
|
||||||
selector:
|
selector:
|
||||||
app: scoresaber-reloaded-frontend
|
app: scoresaber-reloaded
|
@ -1,4 +1,4 @@
|
|||||||
name: "Deploy Frontend"
|
name: "Deploy"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -26,11 +26,10 @@ jobs:
|
|||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./apps/frontend/Dockerfile
|
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
git.fascinated.cc/fascinated/scoresaber-reloaded-frontend:${{ github.sha }}
|
git.fascinated.cc/fascinated/scoresaber-reloaded:${{ github.sha }}
|
||||||
git.fascinated.cc/fascinated/scoresaber-reloaded-frontend:latest
|
git.fascinated.cc/fascinated/scoresaber-reloaded:latest
|
||||||
build-args: |
|
build-args: |
|
||||||
GIT_REV=${{ gitea.sha }}
|
GIT_REV=${{ gitea.sha }}
|
||||||
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
@ -50,9 +49,9 @@ jobs:
|
|||||||
action: deploy
|
action: deploy
|
||||||
namespace: public-services
|
namespace: public-services
|
||||||
manifests: |
|
manifests: |
|
||||||
.gitea/kubernetes/frontend/sealed-secrets.yaml
|
.gitea/kubernetes/sealed-secrets.yaml
|
||||||
.gitea/kubernetes/frontend/deployment.yaml
|
.gitea/kubernetes/deployment.yaml
|
||||||
.gitea/kubernetes/frontend/service.yaml
|
.gitea/kubernetes/service.yaml
|
||||||
.gitea/kubernetes/frontend/ingress.yaml
|
.gitea/kubernetes/ingress.yaml
|
||||||
images: |
|
images: |
|
||||||
git.fascinated.cc/fascinated/scoresaber-reloaded-frontend:${{ github.sha }}
|
git.fascinated.cc/fascinated/scoresaber-reloaded:${{ github.sha }}
|
||||||
|
9
.gitignore
vendored
@ -1,15 +1,16 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
|
|
||||||
# production
|
# production
|
||||||
@ -33,6 +34,8 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
.turbo
|
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
# Sentry Config File
|
||||||
|
.env.sentry-build-plugin
|
||||||
|
65
Dockerfile
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
FROM fascinated/docker-images:nodejs_20_with_pnpm AS base
|
||||||
|
|
||||||
|
# Install dependencies and build tools for canvas
|
||||||
|
FROM base AS deps
|
||||||
|
RUN apk add --no-cache python3 make g++ gcc pkgconfig pixman cairo-dev libjpeg-turbo-dev pango-dev giflib-dev
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json* pnpm-lock.yaml* ./
|
||||||
|
RUN pnpm install --frozen-lockfile --quiet
|
||||||
|
|
||||||
|
# Build from source
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Install runtime dependencies
|
||||||
|
RUN apk add --no-cache cairo pango libjpeg-turbo giflib
|
||||||
|
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
# Add the commit hash
|
||||||
|
ARG GIT_REV
|
||||||
|
ENV GIT_REV=${GIT_REV}
|
||||||
|
|
||||||
|
# Add the sentry auth token
|
||||||
|
ARG SENTRY_AUTH_TOKEN
|
||||||
|
ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
|
||||||
|
|
||||||
|
# Build the app
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
# Final stage to run the app
|
||||||
|
FROM base AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install runtime dependencies
|
||||||
|
RUN apk add --no-cache cairo pango libjpeg-turbo giflib
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
RUN mkdir .next
|
||||||
|
RUN chown nextjs:nodejs .next
|
||||||
|
|
||||||
|
# Add the commit hash
|
||||||
|
ARG GIT_REV
|
||||||
|
ENV GIT_REV=${GIT_REV}
|
||||||
|
|
||||||
|
# Copy the built app from the builder stage
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/next.config.mjs ./next.config.mjs
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
ENV PORT=3000
|
||||||
|
|
||||||
|
CMD ["pnpm", "start"]
|
@ -1,3 +0,0 @@
|
|||||||
node_modules
|
|
||||||
.next
|
|
||||||
.turbo
|
|
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["next/core-web-vitals", "next/typescript"],
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
|
||||||
"@typescript-eslint/no-unused-expressions": "off",
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/no-empty-object-type": "off"
|
|
||||||
}
|
|
||||||
}
|
|
41
apps/frontend/.gitignore
vendored
@ -1,41 +0,0 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
.yarn/install-state.gz
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# debug
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env*.local
|
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# typescript
|
|
||||||
*.tsbuildinfo
|
|
||||||
next-env.d.ts
|
|
||||||
|
|
||||||
.idea
|
|
||||||
|
|
||||||
# Sentry Config File
|
|
||||||
.env.sentry-build-plugin
|
|
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": true,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"useTabs": false,
|
|
||||||
"singleQuote": false,
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"printWidth": 120,
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"arrowParens": "avoid",
|
|
||||||
"jsxSingleQuote": false,
|
|
||||||
"jsxBracketSameLine": false
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
FROM fascinated/docker-images:nodejs_20_with_pnpm
|
|
||||||
|
|
||||||
RUN apk update
|
|
||||||
RUN apk add --no-cache libc6-compat
|
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN pnpm i -g turbo@^2
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Install depends
|
|
||||||
RUN pnpm install
|
|
||||||
|
|
||||||
# Add the commit hash
|
|
||||||
ARG GIT_REV
|
|
||||||
ENV GIT_REV=${GIT_REV}
|
|
||||||
|
|
||||||
# Add the sentry auth token
|
|
||||||
ARG SENTRY_AUTH_TOKEN
|
|
||||||
ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
|
|
||||||
|
|
||||||
RUN pnpm turbo run build --filter=frontend
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
CMD node /app/apps/frontend/.next/standalone/server.js
|
|
@ -1,65 +0,0 @@
|
|||||||
FROM fascinated/docker-images:nodejs_20_with_pnpm AS base
|
|
||||||
|
|
||||||
FROM base AS builder
|
|
||||||
RUN apk update
|
|
||||||
RUN apk add --no-cache libc6-compat
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN pnpm i -g turbo@^2
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Generate a partial monorepo with a pruned lockfile for a target workspace.
|
|
||||||
# Assuming "frontend" is the name entered in the project's package.json: { name: "frontend" }
|
|
||||||
RUN turbo prune frontend --docker
|
|
||||||
|
|
||||||
# Add lockfile and package.json's of isolated subworkspace
|
|
||||||
FROM base AS installer
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Add the commit hash
|
|
||||||
ARG GIT_REV
|
|
||||||
ENV GIT_REV=${GIT_REV}
|
|
||||||
|
|
||||||
# Add the sentry auth token
|
|
||||||
ARG SENTRY_AUTH_TOKEN
|
|
||||||
ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
|
||||||
|
|
||||||
RUN pnpm i -g turbo@^2
|
|
||||||
|
|
||||||
# First install the dependencies (as they change less often)
|
|
||||||
COPY --from=builder /app/out/json/ .
|
|
||||||
RUN pnpm install
|
|
||||||
|
|
||||||
# Build the project
|
|
||||||
COPY --from=builder /app/out/full/ .
|
|
||||||
|
|
||||||
RUN ls -la
|
|
||||||
|
|
||||||
RUN pnpm turbo run build --filter=frontend...
|
|
||||||
|
|
||||||
FROM base AS runner
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
|
||||||
|
|
||||||
# Add the commit hash
|
|
||||||
ARG GIT_REV
|
|
||||||
ENV GIT_REV=${GIT_REV}
|
|
||||||
|
|
||||||
# Don't run production as root
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
|
||||||
RUN adduser --system --uid 1001 nextjs
|
|
||||||
USER nextjs
|
|
||||||
|
|
||||||
# Automatically leverage output traces to reduce image size
|
|
||||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
|
||||||
COPY --from=installer --chown=nextjs:nodejs /app/apps/frontend/.next/standalone ./
|
|
||||||
COPY --from=installer --chown=nextjs:nodejs /app/apps/frontend/.next/static ./apps/frontend/.next/static
|
|
||||||
COPY --from=installer --chown=nextjs:nodejs /app/apps/frontend/public ./apps/frontend/public
|
|
||||||
|
|
||||||
CMD node apps/frontend/server.js
|
|
@ -1,56 +0,0 @@
|
|||||||
import { format } from "@formkit/tempo";
|
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {
|
|
||||||
output: "standalone",
|
|
||||||
experimental: {
|
|
||||||
webpackMemoryOptimizations: true,
|
|
||||||
},
|
|
||||||
images: {
|
|
||||||
remotePatterns: [
|
|
||||||
{
|
|
||||||
protocol: "https",
|
|
||||||
hostname: "cdn.scoresaber.com",
|
|
||||||
port: "",
|
|
||||||
pathname: "/**",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
NEXT_PUBLIC_BUILD_ID: process.env.GIT_REV || "bob",
|
|
||||||
NEXT_PUBLIC_BUILD_TIME: new Date().toLocaleDateString("en-US", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "numeric",
|
|
||||||
timeZoneName: "short",
|
|
||||||
}),
|
|
||||||
NEXT_PUBLIC_BUILD_TIME_SHORT: format(new Date(), {
|
|
||||||
date: "short",
|
|
||||||
time: "short",
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
||||||
|
|
||||||
// export default withSentryConfig(nextConfig, {
|
|
||||||
// org: "scoresaber-reloaded",
|
|
||||||
// project: "frontend",
|
|
||||||
// sentryUrl: "https://glitchtip.fascinated.cc/",
|
|
||||||
// silent: !process.env.CI,
|
|
||||||
// reactComponentAnnotation: {
|
|
||||||
// enabled: true,
|
|
||||||
// },
|
|
||||||
// tunnelRoute: "/monitoring",
|
|
||||||
// hideSourceMaps: true,
|
|
||||||
// disableLogger: true,
|
|
||||||
// sourcemaps: {
|
|
||||||
// disable: true,
|
|
||||||
// },
|
|
||||||
// release: {
|
|
||||||
// create: false,
|
|
||||||
// finalize: false,
|
|
||||||
// },
|
|
||||||
// });
|
|
@ -1,65 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "frontend",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "next dev",
|
|
||||||
"build": "next build",
|
|
||||||
"start": "next start",
|
|
||||||
"lint": "next lint"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@formkit/tempo": "^0.1.2",
|
|
||||||
"@heroicons/react": "^2.1.5",
|
|
||||||
"@hookform/resolvers": "^3.9.0",
|
|
||||||
"@radix-ui/react-avatar": "^1.1.0",
|
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
|
||||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
|
||||||
"@sentry/nextjs": "8",
|
|
||||||
"@tanstack/react-query": "^5.55.4",
|
|
||||||
"@trigger.dev/nextjs": "^3.0.8",
|
|
||||||
"@trigger.dev/react": "^3.0.8",
|
|
||||||
"@trigger.dev/sdk": "^3.0.8",
|
|
||||||
"@uidotdev/usehooks": "^2.4.1",
|
|
||||||
"chart.js": "^4.4.4",
|
|
||||||
"class-variance-authority": "^0.7.0",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"comlink": "^4.4.1",
|
|
||||||
"dexie": "^4.0.8",
|
|
||||||
"dexie-react-hooks": "^1.1.7",
|
|
||||||
"framer-motion": "^11.5.4",
|
|
||||||
"js-cookie": "^3.0.5",
|
|
||||||
"ky": "^1.7.2",
|
|
||||||
"lucide-react": "^0.446.0",
|
|
||||||
"mongoose": "^8.7.0",
|
|
||||||
"next": "15.0.0-rc.0",
|
|
||||||
"next-build-id": "^3.0.0",
|
|
||||||
"next-themes": "^0.3.0",
|
|
||||||
"react": "19.0.0-rc-3edc000d-20240926",
|
|
||||||
"react-chartjs-2": "^5.2.0",
|
|
||||||
"react-dom": "19.0.0-rc-3edc000d-20240926",
|
|
||||||
"react-hook-form": "^7.53.0",
|
|
||||||
"styled-jsx": "^5.1.6",
|
|
||||||
"tailwind-merge": "^2.5.2",
|
|
||||||
"tailwindcss-animate": "^1.0.7",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/js-cookie": "^3.0.6",
|
|
||||||
"@types/node": "^20",
|
|
||||||
"@types/react": "^18",
|
|
||||||
"@types/react-dom": "^18",
|
|
||||||
"eslint": "^8",
|
|
||||||
"eslint-config-next": "14.2.13",
|
|
||||||
"postcss": "^8",
|
|
||||||
"tailwindcss": "^3.4.1",
|
|
||||||
"typescript": "^5"
|
|
||||||
},
|
|
||||||
"trigger.dev": {
|
|
||||||
"endpointId": "scoresaber-reloaded-KB0Z"
|
|
||||||
}
|
|
||||||
}
|
|
7567
apps/frontend/pnpm-lock.yaml
generated
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["config:recommended", ":dependencyDashboard"]
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return <main>hi</main>;
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
import { cache } from "react";
|
|
||||||
import { config } from "../../config";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Proxies all non-localhost images to make them load faster.
|
|
||||||
*
|
|
||||||
* @param originalUrl the original image url
|
|
||||||
* @returns the new image url
|
|
||||||
*/
|
|
||||||
export function getImageUrl(originalUrl: string) {
|
|
||||||
return `${!config.siteUrl.includes("localhost") ? "https://img.fascinated.cc/upload/q_70/" : ""}${originalUrl}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the average color of an image
|
|
||||||
*
|
|
||||||
* @param src the image url
|
|
||||||
* @returns the average color
|
|
||||||
*/
|
|
||||||
export const getAverageColor = cache(async (src: string) => {
|
|
||||||
return {
|
|
||||||
hex: "#fff",
|
|
||||||
};
|
|
||||||
});
|
|
@ -1,61 +0,0 @@
|
|||||||
const diffColors: Record<string, string> = {
|
|
||||||
easy: "#3CB371",
|
|
||||||
normal: "#59b0f4",
|
|
||||||
hard: "#FF6347",
|
|
||||||
expert: "#bf2a42",
|
|
||||||
expertplus: "#8f48db",
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ScoreBadge = {
|
|
||||||
name: string;
|
|
||||||
min: number | null;
|
|
||||||
max: number | null;
|
|
||||||
color: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const scoreBadges: ScoreBadge[] = [
|
|
||||||
{ name: "SS+", min: 95, max: null, color: diffColors.expertplus },
|
|
||||||
{ name: "SS", min: 90, max: 95, color: diffColors.expert },
|
|
||||||
{ name: "S+", min: 85, max: 90, color: diffColors.hard },
|
|
||||||
{ name: "S", min: 80, max: 85, color: diffColors.normal },
|
|
||||||
{ name: "A", min: 70, max: 80, color: diffColors.easy },
|
|
||||||
{ name: "-", min: null, max: 70, color: "hsl(var(--accent))" },
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the color based on the accuracy provided.
|
|
||||||
*
|
|
||||||
* @param acc - The accuracy for the score
|
|
||||||
* @returns The corresponding color for the accuracy.
|
|
||||||
*/
|
|
||||||
export function getScoreBadgeFromAccuracy(acc: number): ScoreBadge {
|
|
||||||
// Check for SS+ first since it has no upper limit
|
|
||||||
if (acc >= 95) {
|
|
||||||
return scoreBadges[0]; // SS+ color
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate through the rest of the badges
|
|
||||||
for (const badge of scoreBadges) {
|
|
||||||
const min = badge.min ?? -Infinity; // Treat null `min` as -Infinity
|
|
||||||
const max = badge.max ?? Infinity; // Treat null `max` as Infinity
|
|
||||||
|
|
||||||
// Check if the accuracy falls within the badge's range
|
|
||||||
if (acc >= min && acc < (max === null ? Infinity : max)) {
|
|
||||||
return badge; // Return the color of the matching badge
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback color if no badge matches (should not happen)
|
|
||||||
return scoreBadges[scoreBadges.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turns the difficulty of a song into a color
|
|
||||||
*
|
|
||||||
* @param diff the difficulty to get the color for
|
|
||||||
* @returns the color for the difficulty
|
|
||||||
*/
|
|
||||||
export function songDifficultyToColor(diff: string) {
|
|
||||||
diff = diff.replace("+", "Plus");
|
|
||||||
return diffColors[diff.toLowerCase() as keyof typeof diffColors];
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
export const CustomizedAxisTick = ({
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
payload,
|
|
||||||
rotateAngle = -45,
|
|
||||||
}: {
|
|
||||||
x?: number;
|
|
||||||
y?: number;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
payload?: any;
|
|
||||||
rotateAngle?: number;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<g transform={`translate(${x},${y})`}>
|
|
||||||
<text x={0} y={0} dy={16} textAnchor="end" fill="#666" transform={`rotate(${rotateAngle})`}>
|
|
||||||
{payload.value}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,32 +0,0 @@
|
|||||||
import ScoreSaberScoreToken from "@/common/model/token/scoresaber/score-saber-score-token";
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
score: ScoreSaberScoreToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function LeaderboardPlayer({ score }: Props) {
|
|
||||||
const player = score.leaderboardPlayerInfo;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Image
|
|
||||||
unoptimized
|
|
||||||
src={player.profilePicture}
|
|
||||||
width={48}
|
|
||||||
height={48}
|
|
||||||
alt="Song Artwork"
|
|
||||||
className="rounded-md min-w-[48px]"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<Link
|
|
||||||
href={`/player/${player.id}`}
|
|
||||||
target="_blank"
|
|
||||||
className="h-fit hover:brightness-75 transition-all transform-gpu"
|
|
||||||
>
|
|
||||||
<p>{player.name}</p>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { scoresaberService } from "@/common/service/impl/scoresaber";
|
|
||||||
import ScoreSaberLeaderboardToken from "@/common/model/token/scoresaber/score-saber-leaderboard-token";
|
|
||||||
import ScoreSaberLeaderboardScoresPageToken from "@/common/model/token/scoresaber/score-saber-leaderboard-scores-page-token";
|
|
||||||
import useWindowDimensions from "@/hooks/use-window-dimensions";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import Card from "../card";
|
|
||||||
import Pagination from "../input/pagination";
|
|
||||||
import LeaderboardScore from "./leaderboard-score";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
leaderboard: ScoreSaberLeaderboardToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function LeaderboardScores({ leaderboard }: Props) {
|
|
||||||
const { width } = useWindowDimensions();
|
|
||||||
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const [currentScores, setCurrentScores] = useState<ScoreSaberLeaderboardScoresPageToken | undefined>();
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: scores,
|
|
||||||
isError,
|
|
||||||
isLoading,
|
|
||||||
refetch,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ["playerScores", leaderboard.id, currentPage],
|
|
||||||
queryFn: () => scoresaberService.lookupLeaderboardScores(leaderboard.id + "", currentPage),
|
|
||||||
staleTime: 30 * 1000, // Cache data for 30 seconds
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (scores) {
|
|
||||||
setCurrentScores(scores);
|
|
||||||
}
|
|
||||||
}, [scores]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refetch();
|
|
||||||
}, [leaderboard, currentPage, refetch]);
|
|
||||||
|
|
||||||
if (currentScores === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div initial={{ opacity: 0, y: -50 }} exit={{ opacity: 0, y: -50 }} animate={{ opacity: 1, y: 0 }}>
|
|
||||||
<Card className="flex gap-2 border border-input mt-2">
|
|
||||||
<div className="text-center">
|
|
||||||
{isError && <p>Oopsies! Something went wrong.</p>}
|
|
||||||
{currentScores.scores.length === 0 && <p>No scores found. Invalid Page?</p>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid min-w-full grid-cols-1 divide-y divide-border">
|
|
||||||
{currentScores.scores.map((playerScore, index) => (
|
|
||||||
<LeaderboardScore key={index} score={playerScore} leaderboard={leaderboard} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Pagination
|
|
||||||
mobilePagination={width < 768}
|
|
||||||
page={currentPage}
|
|
||||||
totalPages={Math.ceil(currentScores.metadata.total / currentScores.metadata.itemsPerPage)}
|
|
||||||
loadingPage={isLoading ? currentPage : undefined}
|
|
||||||
onPageChange={setCurrentPage}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,294 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { formatNumberWithCommas } from "@/common/number-utils";
|
|
||||||
import { CategoryScale, Chart, Legend, LinearScale, LineElement, PointElement, Title, Tooltip } from "chart.js";
|
|
||||||
import { Line } from "react-chartjs-2";
|
|
||||||
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
|
|
||||||
import { getDaysAgo, parseDate } from "@/common/time-utils";
|
|
||||||
import { useIsMobile } from "@/hooks/use-is-mobile";
|
|
||||||
|
|
||||||
Chart.register(LinearScale, CategoryScale, PointElement, LineElement, Title, Tooltip, Legend);
|
|
||||||
|
|
||||||
type AxisPosition = "left" | "right";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A ChartJS axis
|
|
||||||
*/
|
|
||||||
type Axis = {
|
|
||||||
id?: string;
|
|
||||||
position?: AxisPosition;
|
|
||||||
display?: boolean;
|
|
||||||
grid?: { color?: string; drawOnChartArea?: boolean };
|
|
||||||
title?: { display: boolean; text: string; color?: string };
|
|
||||||
ticks?: {
|
|
||||||
stepSize?: number;
|
|
||||||
};
|
|
||||||
reverse?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A ChartJS dataset
|
|
||||||
*/
|
|
||||||
type Dataset = {
|
|
||||||
label: string;
|
|
||||||
data: (number | null)[]; // Allow null values for gaps
|
|
||||||
borderColor: string;
|
|
||||||
fill: boolean;
|
|
||||||
lineTension: number;
|
|
||||||
spanGaps: boolean;
|
|
||||||
yAxisID: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate an axis
|
|
||||||
*
|
|
||||||
* @param id the id of the axis
|
|
||||||
* @param reverse if the axis should be reversed
|
|
||||||
* @param display if the axis should be displayed
|
|
||||||
* @param position the position of the axis
|
|
||||||
* @param displayName the optional name to display for the axis
|
|
||||||
*/
|
|
||||||
const generateAxis = (
|
|
||||||
id: string,
|
|
||||||
reverse: boolean,
|
|
||||||
display: boolean,
|
|
||||||
position: AxisPosition,
|
|
||||||
displayName: string
|
|
||||||
): Axis => ({
|
|
||||||
id,
|
|
||||||
position,
|
|
||||||
display,
|
|
||||||
grid: {
|
|
||||||
drawOnChartArea: id === "y",
|
|
||||||
color: id === "y" ? "#252525" : "",
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: displayName,
|
|
||||||
color: "#ffffff",
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
stepSize: 10,
|
|
||||||
},
|
|
||||||
reverse,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a dataset
|
|
||||||
*
|
|
||||||
* @param label the label of the dataset
|
|
||||||
* @param data the data of the dataset
|
|
||||||
* @param borderColor the border color of the dataset
|
|
||||||
* @param yAxisID the ID of the y-axis
|
|
||||||
*/
|
|
||||||
const generateDataset = (label: string, data: (number | null)[], borderColor: string, yAxisID: string): Dataset => ({
|
|
||||||
label,
|
|
||||||
data,
|
|
||||||
borderColor,
|
|
||||||
fill: false,
|
|
||||||
lineTension: 0.5,
|
|
||||||
spanGaps: false, // Set to false, so we can allow gaps
|
|
||||||
yAxisID,
|
|
||||||
});
|
|
||||||
|
|
||||||
type DatasetConfig = {
|
|
||||||
title: string;
|
|
||||||
field: string;
|
|
||||||
color: string;
|
|
||||||
axisId: string;
|
|
||||||
axisConfig: {
|
|
||||||
reverse: boolean;
|
|
||||||
display: boolean;
|
|
||||||
hideOnMobile?: boolean;
|
|
||||||
displayName: string;
|
|
||||||
position: AxisPosition;
|
|
||||||
};
|
|
||||||
labelFormatter: (value: number) => string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Configuration array for datasets and axes with label formatters
|
|
||||||
const datasetConfig: DatasetConfig[] = [
|
|
||||||
{
|
|
||||||
title: "Rank",
|
|
||||||
field: "rank",
|
|
||||||
color: "#3EC1D3",
|
|
||||||
axisId: "y",
|
|
||||||
axisConfig: {
|
|
||||||
reverse: true,
|
|
||||||
display: true,
|
|
||||||
displayName: "Global Rank",
|
|
||||||
position: "left",
|
|
||||||
},
|
|
||||||
labelFormatter: (value: number) => `Rank #${formatNumberWithCommas(value)}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Country Rank",
|
|
||||||
field: "countryRank",
|
|
||||||
color: "#FFEA00",
|
|
||||||
axisId: "y1",
|
|
||||||
axisConfig: {
|
|
||||||
reverse: true,
|
|
||||||
display: false,
|
|
||||||
displayName: "Country Rank",
|
|
||||||
position: "left",
|
|
||||||
},
|
|
||||||
labelFormatter: (value: number) => `Country Rank #${formatNumberWithCommas(value)}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "PP",
|
|
||||||
field: "pp",
|
|
||||||
color: "#606fff",
|
|
||||||
axisId: "y2",
|
|
||||||
axisConfig: {
|
|
||||||
reverse: false,
|
|
||||||
display: true,
|
|
||||||
hideOnMobile: true,
|
|
||||||
displayName: "PP",
|
|
||||||
position: "right",
|
|
||||||
},
|
|
||||||
labelFormatter: (value: number) => `PP ${formatNumberWithCommas(value)}pp`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
player: ScoreSaberPlayer;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PlayerRankChart({ player }: Props) {
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
if (!player.statisticHistory || Object.keys(player.statisticHistory).length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<p>Unable to load player rank chart, missing data...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const labels: string[] = [];
|
|
||||||
const histories: Record<string, (number | null)[]> = {
|
|
||||||
rank: [],
|
|
||||||
countryRank: [],
|
|
||||||
pp: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const statisticEntries = Object.entries(player.statisticHistory).sort(
|
|
||||||
([a], [b]) => parseDate(a).getTime() - parseDate(b).getTime()
|
|
||||||
);
|
|
||||||
|
|
||||||
let previousDate: Date | null = null;
|
|
||||||
|
|
||||||
// Create labels and history data
|
|
||||||
for (const [dateString, history] of statisticEntries) {
|
|
||||||
const currentDate = parseDate(dateString);
|
|
||||||
|
|
||||||
// Insert nulls for missing days
|
|
||||||
if (previousDate) {
|
|
||||||
const diffDays = Math.floor((currentDate.getTime() - previousDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
||||||
|
|
||||||
for (let i = 1; i < diffDays; i++) {
|
|
||||||
labels.push(`${getDaysAgo(new Date(currentDate.getTime() - i * 24 * 60 * 60 * 1000))} days ago`);
|
|
||||||
datasetConfig.forEach(config => {
|
|
||||||
histories[config.field].push(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const daysAgo = getDaysAgo(currentDate);
|
|
||||||
labels.push(daysAgo === 0 ? "Today" : `${daysAgo} days ago`);
|
|
||||||
|
|
||||||
// stupid typescript crying wahh wahh wahh - https://youtu.be/hBEKgHDzm_s?si=ekOdMMdb-lFnA1Yz&t=11
|
|
||||||
datasetConfig.forEach(config => {
|
|
||||||
(histories as any)[config.field].push((history as any)[config.field] ?? null);
|
|
||||||
});
|
|
||||||
|
|
||||||
previousDate = currentDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamically create axes and datasets based on datasetConfig
|
|
||||||
const axes: Record<string, Axis> = {
|
|
||||||
x: {
|
|
||||||
grid: {
|
|
||||||
color: "#252525", // gray grid lines
|
|
||||||
},
|
|
||||||
reverse: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const datasets: Dataset[] = datasetConfig
|
|
||||||
.map(config => {
|
|
||||||
if (histories[config.field].some(value => value !== null)) {
|
|
||||||
axes[config.axisId] = generateAxis(
|
|
||||||
config.axisId,
|
|
||||||
config.axisConfig.reverse,
|
|
||||||
isMobile && config.axisConfig.hideOnMobile ? false : config.axisConfig.display,
|
|
||||||
config.axisConfig.position,
|
|
||||||
config.axisConfig.displayName
|
|
||||||
);
|
|
||||||
return generateDataset(config.title, histories[config.field], config.color, config.axisId);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.filter(Boolean) as Dataset[];
|
|
||||||
|
|
||||||
const options: any = {
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
responsive: true,
|
|
||||||
interaction: {
|
|
||||||
mode: "index",
|
|
||||||
intersect: false,
|
|
||||||
},
|
|
||||||
scales: axes,
|
|
||||||
elements: {
|
|
||||||
point: {
|
|
||||||
radius: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
position: "top" as const,
|
|
||||||
labels: {
|
|
||||||
color: "white",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
callbacks: {
|
|
||||||
label(context: any) {
|
|
||||||
const value = Number(context.parsed.y);
|
|
||||||
const config = datasetConfig.find(cfg => cfg.title === context.dataset.label);
|
|
||||||
return config?.labelFormatter(value) ?? "";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
labels,
|
|
||||||
datasets,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="block h-[320px] w-full relative">
|
|
||||||
<Line
|
|
||||||
className="max-w-[100%]"
|
|
||||||
options={options}
|
|
||||||
data={data}
|
|
||||||
plugins={[
|
|
||||||
{
|
|
||||||
id: "legend-padding",
|
|
||||||
beforeInit: (chart: any) => {
|
|
||||||
const originalFit = chart.legend.fit;
|
|
||||||
|
|
||||||
chart.legend.fit = function fit() {
|
|
||||||
originalFit.bind(chart.legend)();
|
|
||||||
this.height += 2;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
61
next.config.mjs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { withSentryConfig } from "@sentry/nextjs";
|
||||||
|
import { format } from "@formkit/tempo";
|
||||||
|
import nextBuildId from "next-build-id";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file
|
||||||
|
const __dirname = path.dirname(__filename); // get the name of the directory
|
||||||
|
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
experimental: {
|
||||||
|
webpackMemoryOptimizations: true,
|
||||||
|
},
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "cdn.scoresaber.com",
|
||||||
|
port: "",
|
||||||
|
pathname: "/**",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
NEXT_PUBLIC_BUILD_ID:
|
||||||
|
process.env.GIT_REV || nextBuildId.sync({ dir: __dirname }),
|
||||||
|
NEXT_PUBLIC_BUILD_TIME: new Date().toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric",
|
||||||
|
timeZoneName: "short",
|
||||||
|
}),
|
||||||
|
NEXT_PUBLIC_BUILD_TIME_SHORT: format(new Date(), {
|
||||||
|
date: "short",
|
||||||
|
time: "short",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withSentryConfig(nextConfig, {
|
||||||
|
org: "scoresaber-reloaded",
|
||||||
|
project: "frontend",
|
||||||
|
sentryUrl: "https://glitchtip.fascinated.cc/",
|
||||||
|
silent: !process.env.CI,
|
||||||
|
reactComponentAnnotation: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
tunnelRoute: "/monitoring",
|
||||||
|
hideSourceMaps: true,
|
||||||
|
disableLogger: true,
|
||||||
|
sourcemaps: {
|
||||||
|
disable: true,
|
||||||
|
},
|
||||||
|
release: {
|
||||||
|
create: false,
|
||||||
|
finalize: false,
|
||||||
|
},
|
||||||
|
});
|
78
package.json
@ -1,28 +1,66 @@
|
|||||||
{
|
{
|
||||||
"name": "ssr",
|
"name": "scoresaber-reloadedv3",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": {
|
|
||||||
"nohoist": [
|
|
||||||
"**/frontend",
|
|
||||||
"**/frontend/**"
|
|
||||||
],
|
|
||||||
"packages": [
|
|
||||||
"apps/*",
|
|
||||||
"packages/*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"dev": "next dev --turbo",
|
||||||
"clean": "turbo run clean",
|
"build": "next build",
|
||||||
"dev": "turbo run dev",
|
"start": "next start",
|
||||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
"lint": "next lint"
|
||||||
"lint": "turbo run lint",
|
},
|
||||||
"test": "turbo run test"
|
"dependencies": {
|
||||||
|
"@formkit/tempo": "^0.1.2",
|
||||||
|
"@heroicons/react": "^2.1.5",
|
||||||
|
"@hookform/resolvers": "^3.9.0",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.0",
|
||||||
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||||
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
|
"@sentry/nextjs": "8",
|
||||||
|
"@tanstack/react-query": "^5.55.4",
|
||||||
|
"@trigger.dev/nextjs": "^3.0.8",
|
||||||
|
"@trigger.dev/react": "^3.0.8",
|
||||||
|
"@trigger.dev/sdk": "^3.0.8",
|
||||||
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
|
"canvas": "3.0.0-rc2",
|
||||||
|
"chart.js": "^4.4.4",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"comlink": "^4.4.1",
|
||||||
|
"dexie": "^4.0.8",
|
||||||
|
"dexie-react-hooks": "^1.1.7",
|
||||||
|
"extract-colors": "^4.0.8",
|
||||||
|
"framer-motion": "^11.5.4",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
|
"ky": "^1.7.2",
|
||||||
|
"lucide-react": "^0.447.0",
|
||||||
|
"mongoose": "^8.7.0",
|
||||||
|
"next": "15.0.0-rc.0",
|
||||||
|
"next-build-id": "^3.0.0",
|
||||||
|
"next-themes": "^0.3.0",
|
||||||
|
"react": "19.0.0-rc-3edc000d-20240926",
|
||||||
|
"react-chartjs-2": "^5.2.0",
|
||||||
|
"react-dom": "19.0.0-rc-3edc000d-20240926",
|
||||||
|
"react-hook-form": "^7.53.0",
|
||||||
|
"tailwind-merge": "^2.5.2",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^3.2.5",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"turbo": "^2.0.3"
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "14.2.14",
|
||||||
|
"postcss": "^8",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.12.0"
|
"trigger.dev": {
|
||||||
|
"endpointId": "scoresaber-reloaded-KB0Z"
|
||||||
|
}
|
||||||
}
|
}
|
1533
pnpm-lock.yaml
generated
@ -1,3 +0,0 @@
|
|||||||
packages:
|
|
||||||
- "apps/*"
|
|
||||||
- "packages/*"
|
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 841 B After Width: | Height: | Size: 841 B |
Before Width: | Height: | Size: 132 B After Width: | Height: | Size: 132 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 766 B After Width: | Height: | Size: 766 B |
Before Width: | Height: | Size: 659 B After Width: | Height: | Size: 659 B |
Before Width: | Height: | Size: 604 B After Width: | Height: | Size: 604 B |
Before Width: | Height: | Size: 121 B After Width: | Height: | Size: 121 B |
Before Width: | Height: | Size: 522 B After Width: | Height: | Size: 522 B |
Before Width: | Height: | Size: 445 B After Width: | Height: | Size: 445 B |
Before Width: | Height: | Size: 320 B After Width: | Height: | Size: 320 B |
Before Width: | Height: | Size: 909 B After Width: | Height: | Size: 909 B |
Before Width: | Height: | Size: 109 B After Width: | Height: | Size: 109 B |
Before Width: | Height: | Size: 554 B After Width: | Height: | Size: 554 B |
Before Width: | Height: | Size: 311 B After Width: | Height: | Size: 311 B |
Before Width: | Height: | Size: 179 B After Width: | Height: | Size: 179 B |
Before Width: | Height: | Size: 214 B After Width: | Height: | Size: 214 B |
Before Width: | Height: | Size: 339 B After Width: | Height: | Size: 339 B |
Before Width: | Height: | Size: 324 B After Width: | Height: | Size: 324 B |
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 282 B |
Before Width: | Height: | Size: 127 B After Width: | Height: | Size: 127 B |
Before Width: | Height: | Size: 254 B After Width: | Height: | Size: 254 B |
Before Width: | Height: | Size: 105 B After Width: | Height: | Size: 105 B |
Before Width: | Height: | Size: 326 B After Width: | Height: | Size: 326 B |
Before Width: | Height: | Size: 651 B After Width: | Height: | Size: 651 B |
Before Width: | Height: | Size: 127 B After Width: | Height: | Size: 127 B |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 132 B After Width: | Height: | Size: 132 B |
Before Width: | Height: | Size: 810 B After Width: | Height: | Size: 810 B |
Before Width: | Height: | Size: 792 B After Width: | Height: | Size: 792 B |
Before Width: | Height: | Size: 287 B After Width: | Height: | Size: 287 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 206 B After Width: | Height: | Size: 206 B |
Before Width: | Height: | Size: 139 B After Width: | Height: | Size: 139 B |
Before Width: | Height: | Size: 377 B After Width: | Height: | Size: 377 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 359 B After Width: | Height: | Size: 359 B |
Before Width: | Height: | Size: 561 B After Width: | Height: | Size: 561 B |
Before Width: | Height: | Size: 523 B After Width: | Height: | Size: 523 B |
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 286 B |
Before Width: | Height: | Size: 344 B After Width: | Height: | Size: 344 B |
Before Width: | Height: | Size: 135 B After Width: | Height: | Size: 135 B |
Before Width: | Height: | Size: 123 B After Width: | Height: | Size: 123 B |
Before Width: | Height: | Size: 717 B After Width: | Height: | Size: 717 B |
Before Width: | Height: | Size: 263 B After Width: | Height: | Size: 263 B |
Before Width: | Height: | Size: 215 B After Width: | Height: | Size: 215 B |
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 286 B |
Before Width: | Height: | Size: 135 B After Width: | Height: | Size: 135 B |
Before Width: | Height: | Size: 419 B After Width: | Height: | Size: 419 B |
Before Width: | Height: | Size: 362 B After Width: | Height: | Size: 362 B |
Before Width: | Height: | Size: 444 B After Width: | Height: | Size: 444 B |
Before Width: | Height: | Size: 260 B After Width: | Height: | Size: 260 B |
Before Width: | Height: | Size: 641 B After Width: | Height: | Size: 641 B |
Before Width: | Height: | Size: 486 B After Width: | Height: | Size: 486 B |
Before Width: | Height: | Size: 361 B After Width: | Height: | Size: 361 B |
Before Width: | Height: | Size: 105 B After Width: | Height: | Size: 105 B |
Before Width: | Height: | Size: 530 B After Width: | Height: | Size: 530 B |
Before Width: | Height: | Size: 163 B After Width: | Height: | Size: 163 B |
Before Width: | Height: | Size: 501 B After Width: | Height: | Size: 501 B |
Before Width: | Height: | Size: 459 B After Width: | Height: | Size: 459 B |
Before Width: | Height: | Size: 368 B After Width: | Height: | Size: 368 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 106 B After Width: | Height: | Size: 106 B |
Before Width: | Height: | Size: 323 B After Width: | Height: | Size: 323 B |