Compare commits
8
Commits
435ea78251
...
e73aa28dcd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e73aa28dcd | ||
|
|
4a6fcd050e | ||
|
|
49185dd7a9 | ||
|
|
0aef27efc4 | ||
|
|
3213126a12 | ||
|
|
2f708bcf11 | ||
|
|
7a2c0fd4d4 | ||
|
|
fc7e6ca248 |
No files matched your search
@@ -0,0 +1,27 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { WebsiteSetting } from "@/db/schema";
|
||||||
|
import { db } from "./src/lib/db";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const result = await db
|
||||||
|
.select()
|
||||||
|
.from(WebsiteSetting)
|
||||||
|
.where(eq(WebsiteSetting.key, "habbo_imaging_url"));
|
||||||
|
console.log("Current:", result);
|
||||||
|
|
||||||
|
if (result[0]?.value !== "/imaging") {
|
||||||
|
await db
|
||||||
|
.update(WebsiteSetting)
|
||||||
|
.set({ value: "/imaging" })
|
||||||
|
.where(eq(WebsiteSetting.key, "habbo_imaging_url"));
|
||||||
|
console.log("Updated to /imaging");
|
||||||
|
} else {
|
||||||
|
console.log("Already correct");
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
+9
-9
@@ -44,8 +44,8 @@
|
|||||||
"jpeg-js": "0.4.4",
|
"jpeg-js": "0.4.4",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lenis": "1.3.25",
|
"lenis": "1.3.26",
|
||||||
"lucide-react": "1.28.0",
|
"lucide-react": "1.31.0",
|
||||||
"lzma": "2.3.2",
|
"lzma": "2.3.2",
|
||||||
"motion": "12.43.0",
|
"motion": "12.43.0",
|
||||||
"music-metadata": "11.14.0",
|
"music-metadata": "11.14.0",
|
||||||
@@ -57,11 +57,11 @@
|
|||||||
"pino": "10.3.1",
|
"pino": "10.3.1",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8",
|
"react-dom": "19.2.8",
|
||||||
"react-hook-form": "7.84.0",
|
"react-hook-form": "7.85.0",
|
||||||
"resend": "6.18.1",
|
"resend": "6.18.1",
|
||||||
"server-only": "0.0.1",
|
"server-only": "0.0.1",
|
||||||
"sharp": "0.35.3",
|
"sharp": "0.35.3",
|
||||||
"sonner": "2.0.7",
|
"sonner": "2.0.8",
|
||||||
"tailwind-merge": "3.6.0",
|
"tailwind-merge": "3.6.0",
|
||||||
"tailwindcss-animate": "1.0.7",
|
"tailwindcss-animate": "1.0.7",
|
||||||
"zod": "4.4.3"
|
"zod": "4.4.3"
|
||||||
@@ -71,21 +71,21 @@
|
|||||||
"@tailwindcss/forms": "^0.5.11",
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"@tailwindcss/postcss": "^4.3.3",
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
"@tailwindcss/typography": "^0.5.20",
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
"@types/node": "^26.1.2",
|
"@types/node": "^26.2.0",
|
||||||
"@types/react": "19.2.18",
|
"@types/react": "19.2.18",
|
||||||
"@types/react-dom": "19.2.4",
|
"@types/react-dom": "19.2.4",
|
||||||
"@vitest/coverage-v8": "4.1.10",
|
"@vitest/coverage-v8": "4.1.10",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"knip": "6.31.0",
|
"knip": "6.32.0",
|
||||||
"lint-staged": "^17.3.0",
|
"lint-staged": "^17.3.0",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"postcss": "^8.5.25",
|
"postcss": "^8.5.26",
|
||||||
"tailwindcss": "^4.3.3",
|
"tailwindcss": "^4.3.3",
|
||||||
"tsx": "^4.23.5",
|
"tsx": "^4.23.12",
|
||||||
"typescript": "^7.0.2",
|
"typescript": "^7.0.2",
|
||||||
"vite": "8.2.0",
|
"vite": "8.2.1",
|
||||||
"vitest": "4.1.10"
|
"vitest": "4.1.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+90
-80
@@ -36,7 +36,7 @@ importers:
|
|||||||
version: 3.2.2([email protected])
|
version: 3.2.2([email protected])
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: 5.7.1
|
specifier: 5.7.1
|
||||||
version: 5.7.1(@standard-schema/[email protected])([email protected]4.0([email protected]))([email protected])
|
version: 5.7.1(@standard-schema/[email protected])([email protected]5.0([email protected]))([email protected])
|
||||||
'@next/bundle-analyzer':
|
'@next/bundle-analyzer':
|
||||||
specifier: 16.3.0
|
specifier: 16.3.0
|
||||||
version: 16.3.0
|
version: 16.3.0
|
||||||
@@ -60,7 +60,7 @@ importers:
|
|||||||
version: 3.4.13
|
version: 3.4.13
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: 0.45.2
|
specifier: 0.45.2
|
||||||
version: 0.45.2([email protected](@types/node@26.1.2))
|
version: 0.45.2([email protected](@types/node@26.2.0))
|
||||||
hash-wasm:
|
hash-wasm:
|
||||||
specifier: 4.12.0
|
specifier: 4.12.0
|
||||||
version: 4.12.0
|
version: 4.12.0
|
||||||
@@ -77,11 +77,11 @@ importers:
|
|||||||
specifier: 3.10.1
|
specifier: 3.10.1
|
||||||
version: 3.10.1
|
version: 3.10.1
|
||||||
lenis:
|
lenis:
|
||||||
specifier: 1.3.25
|
specifier: 1.3.26
|
||||||
version: 1.3.25([email protected])
|
version: 1.3.26([email protected])
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: 1.28.0
|
specifier: 1.31.0
|
||||||
version: 1.28.0([email protected])
|
version: 1.31.0([email protected])
|
||||||
lzma:
|
lzma:
|
||||||
specifier: 2.3.2
|
specifier: 2.3.2
|
||||||
version: 2.3.2
|
version: 2.3.2
|
||||||
@@ -93,16 +93,16 @@ importers:
|
|||||||
version: 11.14.0([email protected])
|
version: 11.14.0([email protected])
|
||||||
mysql2:
|
mysql2:
|
||||||
specifier: 3.23.2
|
specifier: 3.23.2
|
||||||
version: 3.23.2(@types/node@26.1.2)
|
version: 3.23.2(@types/node@26.2.0)
|
||||||
next:
|
next:
|
||||||
specifier: 16.3.0
|
specifier: 16.3.0
|
||||||
version: 16.3.0(@types/node@26.1.2)([email protected])([email protected]([email protected]))([email protected])
|
version: 16.3.0(@types/node@26.2.0)([email protected])([email protected]([email protected]))([email protected])
|
||||||
next-auth:
|
next-auth:
|
||||||
specifier: 5.0.0-beta.32
|
specifier: 5.0.0-beta.32
|
||||||
version: 5.0.0-beta.32([email protected](@types/node@26.1.2)([email protected])([email protected]([email protected]))([email protected]))([email protected])
|
version: 5.0.0-beta.32([email protected](@types/node@26.2.0)([email protected])([email protected]([email protected]))([email protected]))([email protected])
|
||||||
next-intl:
|
next-intl:
|
||||||
specifier: 4.13.5
|
specifier: 4.13.5
|
||||||
version: 4.13.5([email protected](@types/node@26.1.2)([email protected])([email protected]([email protected]))([email protected]))([email protected])([email protected])
|
version: 4.13.5([email protected](@types/node@26.2.0)([email protected])([email protected]([email protected]))([email protected]))([email protected])([email protected])
|
||||||
otplib:
|
otplib:
|
||||||
specifier: 13.4.1
|
specifier: 13.4.1
|
||||||
version: 13.4.1
|
version: 13.4.1
|
||||||
@@ -116,8 +116,8 @@ importers:
|
|||||||
specifier: 19.2.8
|
specifier: 19.2.8
|
||||||
version: 19.2.8([email protected])
|
version: 19.2.8([email protected])
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: 7.84.0
|
specifier: 7.85.0
|
||||||
version: 7.84.0([email protected])
|
version: 7.85.0([email protected])
|
||||||
resend:
|
resend:
|
||||||
specifier: 6.18.1
|
specifier: 6.18.1
|
||||||
version: 6.18.1
|
version: 6.18.1
|
||||||
@@ -126,10 +126,10 @@ importers:
|
|||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
sharp:
|
sharp:
|
||||||
specifier: ^0.35.3
|
specifier: ^0.35.3
|
||||||
version: 0.35.3(@types/node@26.1.2)
|
version: 0.35.3(@types/node@26.2.0)
|
||||||
sonner:
|
sonner:
|
||||||
specifier: 2.0.7
|
specifier: 2.0.8
|
||||||
version: 2.0.7([email protected]([email protected]))([email protected])
|
version: 2.0.8(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: 3.6.0
|
specifier: 3.6.0
|
||||||
version: 3.6.0
|
version: 3.6.0
|
||||||
@@ -153,8 +153,8 @@ importers:
|
|||||||
specifier: ^0.5.20
|
specifier: ^0.5.20
|
||||||
version: 0.5.20([email protected])
|
version: 0.5.20([email protected])
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^26.1.2
|
specifier: ^26.2.0
|
||||||
version: 26.1.2
|
version: 26.2.0
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 19.2.18
|
specifier: 19.2.18
|
||||||
version: 19.2.18
|
version: 19.2.18
|
||||||
@@ -174,8 +174,8 @@ importers:
|
|||||||
specifier: ^9.1.7
|
specifier: ^9.1.7
|
||||||
version: 9.1.7
|
version: 9.1.7
|
||||||
knip:
|
knip:
|
||||||
specifier: 6.31.0
|
specifier: 6.32.0
|
||||||
version: 6.31.0
|
version: 6.32.0
|
||||||
lint-staged:
|
lint-staged:
|
||||||
specifier: ^17.3.0
|
specifier: ^17.3.0
|
||||||
version: 17.3.0
|
version: 17.3.0
|
||||||
@@ -189,17 +189,17 @@ importers:
|
|||||||
specifier: ^4.3.3
|
specifier: ^4.3.3
|
||||||
version: 4.3.3
|
version: 4.3.3
|
||||||
tsx:
|
tsx:
|
||||||
specifier: ^4.23.5
|
specifier: ^4.23.12
|
||||||
version: 4.23.5
|
version: 4.23.12
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^7.0.2
|
specifier: ^7.0.2
|
||||||
version: 7.0.2
|
version: 7.0.2
|
||||||
vite:
|
vite:
|
||||||
specifier: 8.2.0
|
specifier: 8.2.1
|
||||||
version: 8.2.0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected])
|
version: 8.2.1(@types/node@26.2.0)([email protected])([email protected])([email protected].12)([email protected])
|
||||||
vitest:
|
vitest:
|
||||||
specifier: 4.1.10
|
specifier: 4.1.10
|
||||||
version: 4.1.10(@types/node@26.1.2)(@vitest/[email protected])([email protected].0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected]))
|
version: 4.1.10(@types/node@26.2.0)(@vitest/[email protected])([email protected].1(@types/node@26.2.0)([email protected])([email protected])([email protected].12)([email protected]))
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -1731,8 +1731,8 @@ packages:
|
|||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||||
|
|
||||||
'@types/node@26.1.2':
|
'@types/node@26.2.0':
|
||||||
resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==}
|
resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==}
|
||||||
|
|
||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==}
|
resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==}
|
||||||
@@ -2212,9 +2212,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
[email protected]:
|
|
||||||
resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==}
|
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A==}
|
resolution: {integrity: sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A==}
|
||||||
|
|
||||||
@@ -2321,13 +2318,13 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||||
|
|
||||||
[email protected]1.0:
|
[email protected]2.0:
|
||||||
resolution: {integrity: sha512-NbeIEmUS2VUMjAkbiSNOKPJeV9wpCsr0660sUyKyMQbk4Iom0++nTLInVp4MJ+LfR4kORnw67bDi5tvO7YLnzA==}
|
resolution: {integrity: sha512-KDX9OmmOFmlvmxTkrx6Z0GHISMut+pXMSKR8eg84bovaxJKx2NdQD4JYCXveSbvieRe107W6vCD2xCpmz0qBYA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
[email protected]5:
|
[email protected]6:
|
||||||
resolution: {integrity: sha512-mOKxayErlaONK8fm4LN3XNd99Qu4plTpn9h9qf8wxzjGrJDzuD84FYzZ81HCd6ZsWp++VWVwOzL286Pf2s2u4A==}
|
resolution: {integrity: sha512-s/xTCZCxTFvHbAN1OzuhNaN5YPJH2ail0XAkctKW1b+RUAG4nUL5UHLXwNko1h8aEeT2jspBXegMgPJd8zcuag==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@nuxt/kit': '>=3.0.0'
|
'@nuxt/kit': '>=3.0.0'
|
||||||
react: '>=17.0.0'
|
react: '>=17.0.0'
|
||||||
@@ -2503,8 +2500,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==}
|
resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==}
|
||||||
engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'}
|
engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'}
|
||||||
|
|
||||||
lucide-react@1.28.0:
|
lucide-react@1.31.0:
|
||||||
resolution: {integrity: sha512-fARAFJULsGuDDydjp6+6blekG/sBIM29TerzLjc9bQUKAcEfrSc4ZQKb25KRz4OMKd87cZTb5dgq0w/T6KufVg==}
|
resolution: {integrity: sha512-G8u2eEtoHUnUa9f8lbvqDhCiORMnYLdUEo06EEG9MQvHQrInKcX3Pa2TH39MM5qyzRcWETxB0+aOwAPI1g1kEg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
@@ -2730,8 +2727,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.2.8
|
react: ^19.2.8
|
||||||
|
|
||||||
[email protected]4.0:
|
[email protected]5.0:
|
||||||
resolution: {integrity: sha512-+hWvQP6GLco56mDwrbU4XnHix8t1z90ltZsDIrREl+jnQFQxYLX8oAzqe/Xn8nHpmoXTY5M6oEXrAhbP1qevNQ==}
|
resolution: {integrity: sha512-U2MTriFXnclmV4rOE20p2DcRFv5WEg3FIcBFOKcOLFHDVvGIMPvLTkTWefUsonmlaVy23khVDxDWym6uJVGOzw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||||
@@ -2854,11 +2851,15 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
|
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
|
||||||
|
|
||||||
[email protected].7:
|
[email protected].8:
|
||||||
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
|
resolution: {integrity: sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
'@types/react': 19.2.18
|
||||||
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
@@ -2968,8 +2969,13 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
[email protected].5:
|
[email protected].11:
|
||||||
resolution: {integrity: sha512-rw55FUaqOoI7RvlQwLbhO4nSDApnQ4/CykPuiQ/EPvtrX3WA9Ig55jIt9VvbBJbzJuj12ueRu4PMZ2SxPVbihg==}
|
resolution: {integrity: sha512-Ry2oTEUnhBdeEdWIztY8kf3/nBGnPnjMLVGL0YfdRXMORuPER5NlKmayqxtxRxwB1xBN+RivRaJfe7PM1rtiyw==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -3022,8 +3028,8 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
[email protected].0:
|
[email protected].1:
|
||||||
resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==}
|
resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3402,10 +3408,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@formatjs/fast-memoize': 3.1.7
|
'@formatjs/fast-memoize': 3.1.7
|
||||||
|
|
||||||
'@hookform/[email protected](@standard-schema/[email protected])([email protected]4.0([email protected]))([email protected])':
|
'@hookform/[email protected](@standard-schema/[email protected])([email protected]5.0([email protected]))([email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@standard-schema/utils': 0.3.0
|
'@standard-schema/utils': 0.3.0
|
||||||
react-hook-form: 7.84.0([email protected])
|
react-hook-form: 7.85.0([email protected])
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@standard-schema/spec': 1.1.0
|
'@standard-schema/spec': 1.1.0
|
||||||
zod: 4.4.3
|
zod: 4.4.3
|
||||||
@@ -4160,7 +4166,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/[email protected]': {}
|
'@types/[email protected]': {}
|
||||||
|
|
||||||
'@types/node@26.1.2':
|
'@types/node@26.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 8.3.0
|
undici-types: 8.3.0
|
||||||
|
|
||||||
@@ -4247,7 +4253,7 @@ snapshots:
|
|||||||
obug: 2.1.4
|
obug: 2.1.4
|
||||||
std-env: 4.2.0
|
std-env: 4.2.0
|
||||||
tinyrainbow: 3.1.1
|
tinyrainbow: 3.1.1
|
||||||
vitest: 4.1.10(@types/node@26.1.2)(@vitest/[email protected])([email protected].0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected]))
|
vitest: 4.1.10(@types/node@26.2.0)(@vitest/[email protected])([email protected].1(@types/node@26.2.0)([email protected])([email protected])([email protected].12)([email protected]))
|
||||||
|
|
||||||
'@vitest/[email protected]':
|
'@vitest/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4258,13 +4264,13 @@ snapshots:
|
|||||||
chai: 6.2.2
|
chai: 6.2.2
|
||||||
tinyrainbow: 3.1.1
|
tinyrainbow: 3.1.1
|
||||||
|
|
||||||
'@vitest/[email protected]([email protected].0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected]))':
|
'@vitest/[email protected]([email protected].1(@types/node@26.2.0)([email protected])([email protected])([email protected].12)([email protected]))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 4.1.10
|
'@vitest/spy': 4.1.10
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 8.2.0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected])
|
vite: 8.2.1(@types/node@26.2.0)([email protected])([email protected])([email protected].12)([email protected])
|
||||||
|
|
||||||
'@vitest/[email protected]':
|
'@vitest/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4390,11 +4396,11 @@ snapshots:
|
|||||||
'@drizzle-team/brocli': 0.10.2
|
'@drizzle-team/brocli': 0.10.2
|
||||||
'@esbuild-kit/esm-loader': 2.6.5
|
'@esbuild-kit/esm-loader': 2.6.5
|
||||||
esbuild: 0.25.12
|
esbuild: 0.25.12
|
||||||
tsx: 4.23.5
|
tsx: 4.23.11
|
||||||
|
|
||||||
[email protected]([email protected](@types/node@26.1.2)):
|
[email protected]([email protected](@types/node@26.2.0)):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
mysql2: 3.23.2(@types/node@26.1.2)
|
mysql2: 3.23.2(@types/node@26.2.0)
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
@@ -4491,10 +4497,6 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
|
||||||
dependencies:
|
|
||||||
resolve-pkg-maps: 1.0.0
|
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
resolve-pkg-maps: 1.0.0
|
resolve-pkg-maps: 1.0.0
|
||||||
@@ -4589,11 +4591,11 @@ snapshots:
|
|||||||
readable-stream: 2.3.8
|
readable-stream: 2.3.8
|
||||||
setimmediate: 1.0.5
|
setimmediate: 1.0.5
|
||||||
|
|
||||||
[email protected]1.0:
|
[email protected]2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0([email protected])
|
fdir: 6.5.0([email protected])
|
||||||
formatly: 0.3.0
|
formatly: 0.3.0
|
||||||
get-tsconfig: 4.14.0
|
get-tsconfig: 4.14.1
|
||||||
jiti: 2.7.0
|
jiti: 2.7.0
|
||||||
oxc-parser: 0.142.0
|
oxc-parser: 0.142.0
|
||||||
oxc-resolver: 11.24.2
|
oxc-resolver: 11.24.2
|
||||||
@@ -4605,7 +4607,7 @@ snapshots:
|
|||||||
yaml: 2.9.0
|
yaml: 2.9.0
|
||||||
zod: 4.4.3
|
zod: 4.4.3
|
||||||
|
|
||||||
[email protected]5([email protected]):
|
[email protected]6([email protected]):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
|
|
||||||
@@ -4723,7 +4725,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
lucide-react@1.28.0([email protected]):
|
lucide-react@1.31.0([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
|
|
||||||
@@ -4782,9 +4784,9 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
[email protected](@types/node@26.1.2):
|
[email protected](@types/node@26.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 26.1.2
|
'@types/node': 26.2.0
|
||||||
aws-ssl-profiles: 1.1.2
|
aws-ssl-profiles: 1.1.2
|
||||||
denque: 2.1.0
|
denque: 2.1.0
|
||||||
generate-function: 2.3.1
|
generate-function: 2.3.1
|
||||||
@@ -4802,22 +4804,22 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]([email protected](@types/node@26.1.2)([email protected])([email protected]([email protected]))([email protected]))([email protected]):
|
[email protected]([email protected](@types/node@26.2.0)([email protected])([email protected]([email protected]))([email protected]))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@auth/core': 0.41.3
|
'@auth/core': 0.41.3
|
||||||
next: 16.3.0(@types/node@26.1.2)([email protected])([email protected]([email protected]))([email protected])
|
next: 16.3.0(@types/node@26.2.0)([email protected])([email protected]([email protected]))([email protected])
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]([email protected](@types/node@26.1.2)([email protected])([email protected]([email protected]))([email protected]))([email protected])([email protected]):
|
[email protected]([email protected](@types/node@26.2.0)([email protected])([email protected]([email protected]))([email protected]))([email protected])([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@formatjs/intl-localematcher': 0.8.13
|
'@formatjs/intl-localematcher': 0.8.13
|
||||||
'@parcel/watcher': 2.6.0
|
'@parcel/watcher': 2.6.0
|
||||||
'@swc/core': 1.15.47
|
'@swc/core': 1.15.47
|
||||||
icu-minify: 4.13.5
|
icu-minify: 4.13.5
|
||||||
negotiator: 1.0.0
|
negotiator: 1.0.0
|
||||||
next: 16.3.0(@types/node@26.1.2)([email protected])([email protected]([email protected]))([email protected])
|
next: 16.3.0(@types/node@26.2.0)([email protected])([email protected]([email protected]))([email protected])
|
||||||
next-intl-swc-plugin-extractor: 4.13.5
|
next-intl-swc-plugin-extractor: 4.13.5
|
||||||
po-parser: 2.1.1
|
po-parser: 2.1.1
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
@@ -4827,7 +4829,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@swc/helpers'
|
- '@swc/helpers'
|
||||||
|
|
||||||
[email protected](@types/node@26.1.2)([email protected])([email protected]([email protected]))([email protected]):
|
[email protected](@types/node@26.2.0)([email protected])([email protected]([email protected]))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 16.3.0
|
'@next/env': 16.3.0
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
@@ -4847,7 +4849,7 @@ snapshots:
|
|||||||
'@next/swc-win32-arm64-msvc': 16.3.0
|
'@next/swc-win32-arm64-msvc': 16.3.0
|
||||||
'@next/swc-win32-x64-msvc': 16.3.0
|
'@next/swc-win32-x64-msvc': 16.3.0
|
||||||
babel-plugin-react-compiler: 1.0.0
|
babel-plugin-react-compiler: 1.0.0
|
||||||
sharp: 0.35.3(@types/node@26.1.2)
|
sharp: 0.35.3(@types/node@26.2.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
@@ -5004,7 +5006,7 @@ snapshots:
|
|||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
scheduler: 0.27.0
|
scheduler: 0.27.0
|
||||||
|
|
||||||
[email protected]4.0([email protected]):
|
[email protected]5.0([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
|
|
||||||
@@ -5098,7 +5100,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected](@types/node@26.1.2):
|
[email protected](@types/node@26.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@img/colour': 1.1.0
|
'@img/colour': 1.1.0
|
||||||
detect-libc: 2.1.2
|
detect-libc: 2.1.2
|
||||||
@@ -5129,7 +5131,7 @@ snapshots:
|
|||||||
'@img/sharp-win32-arm64': 0.35.3
|
'@img/sharp-win32-arm64': 0.35.3
|
||||||
'@img/sharp-win32-ia32': 0.35.3
|
'@img/sharp-win32-ia32': 0.35.3
|
||||||
'@img/sharp-win32-x64': 0.35.3
|
'@img/sharp-win32-x64': 0.35.3
|
||||||
'@types/node': 26.1.2
|
'@types/node': 26.2.0
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
@@ -5145,10 +5147,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
atomic-sleep: 1.0.0
|
atomic-sleep: 1.0.0
|
||||||
|
|
||||||
[email protected].7([email protected]([email protected]))([email protected]):
|
[email protected].8(@types/[email protected])([email protected]([email protected]))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
react-dom: 19.2.8([email protected])
|
react-dom: 19.2.8([email protected])
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.18
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
@@ -5230,7 +5234,13 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected].5:
|
[email protected].11:
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.25.12
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.12
|
esbuild: 0.25.12
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -5294,7 +5304,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected].0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected]):
|
[email protected].1(@types/node@26.2.0)([email protected])([email protected])([email protected].12)([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
lightningcss: 1.33.0
|
lightningcss: 1.33.0
|
||||||
picomatch: 4.0.5
|
picomatch: 4.0.5
|
||||||
@@ -5302,17 +5312,17 @@ snapshots:
|
|||||||
rolldown: 1.2.2
|
rolldown: 1.2.2
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 26.1.2
|
'@types/node': 26.2.0
|
||||||
esbuild: 0.25.12
|
esbuild: 0.25.12
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.7.0
|
jiti: 2.7.0
|
||||||
tsx: 4.23.5
|
tsx: 4.23.12
|
||||||
yaml: 2.9.0
|
yaml: 2.9.0
|
||||||
|
|
||||||
[email protected](@types/node@26.1.2)(@vitest/[email protected])([email protected].0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected])):
|
[email protected](@types/node@26.2.0)(@vitest/[email protected])([email protected].1(@types/node@26.2.0)([email protected])([email protected])([email protected].12)([email protected])):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.1.10
|
'@vitest/expect': 4.1.10
|
||||||
'@vitest/mocker': 4.1.10([email protected].0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected]))
|
'@vitest/mocker': 4.1.10([email protected].1(@types/node@26.2.0)([email protected])([email protected])([email protected].12)([email protected]))
|
||||||
'@vitest/pretty-format': 4.1.10
|
'@vitest/pretty-format': 4.1.10
|
||||||
'@vitest/runner': 4.1.10
|
'@vitest/runner': 4.1.10
|
||||||
'@vitest/snapshot': 4.1.10
|
'@vitest/snapshot': 4.1.10
|
||||||
@@ -5329,10 +5339,10 @@ snapshots:
|
|||||||
tinyexec: 1.3.0
|
tinyexec: 1.3.0
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
tinyrainbow: 3.1.1
|
tinyrainbow: 3.1.1
|
||||||
vite: 8.2.0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected])
|
vite: 8.2.1(@types/node@26.2.0)([email protected])([email protected])([email protected].12)([email protected])
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 26.1.2
|
'@types/node': 26.2.0
|
||||||
'@vitest/coverage-v8': 4.1.10([email protected])
|
'@vitest/coverage-v8': 4.1.10([email protected])
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- msw
|
- msw
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui";
|
|||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { db, MessengerFriendships, User } from "@/lib/db";
|
import { db, MessengerFriendships, User } from "@/lib/db";
|
||||||
import { avatarImageUrl } from "@/lib/format";
|
import { avatarImageUrl } from "@/lib/format";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
|
||||||
|
|
||||||
export const metadata = { title: "Friends" };
|
export const metadata = { title: "Friends" };
|
||||||
|
|
||||||
@@ -66,26 +65,20 @@ export default async function FriendsPage({
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const [friends, imagerBase] = await Promise.all([
|
const friends = friendIds.length
|
||||||
friendIds.length
|
? await db
|
||||||
? db
|
.select({
|
||||||
.select({
|
id: User.id,
|
||||||
id: User.id,
|
username: User.username,
|
||||||
username: User.username,
|
look: User.look,
|
||||||
look: User.look,
|
motto: User.motto,
|
||||||
motto: User.motto,
|
online: User.online,
|
||||||
online: User.online,
|
})
|
||||||
})
|
.from(User)
|
||||||
.from(User)
|
.where(inArray(User.id, friendIds))
|
||||||
.where(inArray(User.id, friendIds))
|
.orderBy(asc(User.username))
|
||||||
.orderBy(asc(User.username))
|
.catch(() => [])
|
||||||
.catch(() => [])
|
: [];
|
||||||
: Promise.resolve([]),
|
|
||||||
siteSettings.get(
|
|
||||||
"habbo_imaging_url",
|
|
||||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error === "not_found"
|
error === "not_found"
|
||||||
@@ -125,7 +118,7 @@ export default async function FriendsPage({
|
|||||||
) : (
|
) : (
|
||||||
<div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}>
|
<div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}>
|
||||||
{friends.map((friend) => {
|
{friends.map((friend) => {
|
||||||
const avatar = avatarImageUrl(imagerBase ?? "", friend.look, {
|
const avatar = avatarImageUrl(friend.look, {
|
||||||
size: "s",
|
size: "s",
|
||||||
headOnly: true,
|
headOnly: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui";
|
|||||||
import { db, Guilds, GuildsMembers, User } from "@/lib/db";
|
import { db, Guilds, GuildsMembers, User } from "@/lib/db";
|
||||||
import { avatarImageUrl } from "@/lib/format";
|
import { avatarImageUrl } from "@/lib/format";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
|
||||||
|
|
||||||
type GuildView = {
|
type GuildView = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -98,11 +97,6 @@ export default async function GuildPage({
|
|||||||
users = [];
|
users = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const imagerBase = await siteSettings.get(
|
|
||||||
"habbo_imaging_url",
|
|
||||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
|
||||||
);
|
|
||||||
|
|
||||||
const usersById = new Map(users.map((u) => [u.id, u]));
|
const usersById = new Map(users.map((u) => [u.id, u]));
|
||||||
const sinceById = new Map(memberRows.map((m) => [m.userId, m.memberSince]));
|
const sinceById = new Map(memberRows.map((m) => [m.userId, m.memberSince]));
|
||||||
|
|
||||||
@@ -211,7 +205,7 @@ export default async function GuildPage({
|
|||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img
|
<img
|
||||||
className="avatar"
|
className="avatar"
|
||||||
src={avatarImageUrl(imagerBase ?? "", m.look, { size: "l" })}
|
src={avatarImageUrl(m.look)}
|
||||||
alt={`${m.username} avatar`}
|
alt={`${m.username} avatar`}
|
||||||
width={50}
|
width={50}
|
||||||
height={75}
|
height={75}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { getTranslations } from "next-intl/server";
|
|||||||
import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui";
|
import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui";
|
||||||
import { db, User, UsersCurrency, UsersSettings } from "@/lib/db";
|
import { db, User, UsersCurrency, UsersSettings } from "@/lib/db";
|
||||||
import { avatarImageUrl } from "@/lib/format";
|
import { avatarImageUrl } from "@/lib/format";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
|
||||||
|
|
||||||
export const metadata = { title: "Leaderboard" };
|
export const metadata = { title: "Leaderboard" };
|
||||||
|
|
||||||
@@ -158,20 +157,14 @@ export default async function LeaderboardPage({
|
|||||||
: "credits";
|
: "credits";
|
||||||
const activeTab = TABS.find((tab) => tab.key === active) ?? TABS[0];
|
const activeTab = TABS.find((tab) => tab.key === active) ?? TABS[0];
|
||||||
|
|
||||||
const [rows, imagerBase] = await Promise.all([
|
const rows = await (active === "credits"
|
||||||
active === "credits"
|
? loadCreditsRows()
|
||||||
? loadCreditsRows()
|
: active === "diamonds" || active === "duckets"
|
||||||
: active === "diamonds" || active === "duckets"
|
? // eslint-disable-next-line security/detect-object-injection -- active validated as "diamonds"|"duckets" in this branch
|
||||||
? // eslint-disable-next-line security/detect-object-injection -- active validated as "diamonds"|"duckets" in this branch
|
loadCurrencyRows(CURRENCY_TYPE[active])
|
||||||
loadCurrencyRows(CURRENCY_TYPE[active])
|
: loadSettingsRows(
|
||||||
: loadSettingsRows(
|
SETTINGS_FIELD[active as keyof typeof SETTINGS_FIELD],
|
||||||
SETTINGS_FIELD[active as keyof typeof SETTINGS_FIELD],
|
));
|
||||||
),
|
|
||||||
siteSettings.get(
|
|
||||||
"habbo_imaging_url",
|
|
||||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const valueOnly =
|
const valueOnly =
|
||||||
active === "achievement" ||
|
active === "achievement" ||
|
||||||
@@ -213,7 +206,7 @@ export default async function LeaderboardPage({
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rows.map((row, i) => {
|
{rows.map((row, i) => {
|
||||||
const avatar = avatarImageUrl(imagerBase ?? "", row.look, {
|
const avatar = avatarImageUrl(row.look, {
|
||||||
size: "s",
|
size: "s",
|
||||||
headOnly: true,
|
headOnly: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,17 +18,11 @@ import { siteSettings } from "@/lib/services/site-settings";
|
|||||||
export default async function LoginPage() {
|
export default async function LoginPage() {
|
||||||
const t = await getTranslations("pages.login");
|
const t = await getTranslations("pages.login");
|
||||||
const th = await getTranslations("header");
|
const th = await getTranslations("header");
|
||||||
const [hotelName, imagerRaw, cfg, logo] = await Promise.all([
|
const [hotelName, cfg, logo] = await Promise.all([
|
||||||
resolveHotelName(),
|
resolveHotelName(),
|
||||||
siteSettings.get(
|
|
||||||
"habbo_imaging_url",
|
|
||||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
|
||||||
),
|
|
||||||
captchaConfig(),
|
captchaConfig(),
|
||||||
siteSettings.get("radio_social_discord", ""),
|
|
||||||
siteSettings.get("cms_logo", ""),
|
siteSettings.get("cms_logo", ""),
|
||||||
]);
|
]);
|
||||||
const imager = imagerRaw ?? "https://www.habbo.com/habbo-imaging/avatarimage";
|
|
||||||
const nonce = (await headers()).get("x-nonce") ?? undefined;
|
const nonce = (await headers()).get("x-nonce") ?? undefined;
|
||||||
|
|
||||||
const [online, recentUsers, latestUsers] = await Promise.all([
|
const [online, recentUsers, latestUsers] = await Promise.all([
|
||||||
@@ -234,7 +228,7 @@ export default async function LoginPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={avatarImageUrl(imager, u.look, {
|
src={avatarImageUrl(u.look, {
|
||||||
headOnly: true,
|
headOnly: true,
|
||||||
direction: 2,
|
direction: 2,
|
||||||
size: "s",
|
size: "s",
|
||||||
@@ -305,7 +299,7 @@ export default async function LoginPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={avatarImageUrl(imager, u.look, {
|
src={avatarImageUrl(u.look, {
|
||||||
headOnly: true,
|
headOnly: true,
|
||||||
direction: 2,
|
direction: 2,
|
||||||
size: "s",
|
size: "s",
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ export default async function MePage({
|
|||||||
|
|
||||||
const [
|
const [
|
||||||
userRows,
|
userRows,
|
||||||
imagerBase,
|
|
||||||
hotelName,
|
hotelName,
|
||||||
neededRows,
|
neededRows,
|
||||||
amountRows,
|
amountRows,
|
||||||
@@ -111,10 +110,6 @@ export default async function MePage({
|
|||||||
.from(User)
|
.from(User)
|
||||||
.where(eq(User.id, userId))
|
.where(eq(User.id, userId))
|
||||||
.limit(1),
|
.limit(1),
|
||||||
siteSettings.get(
|
|
||||||
"habbo_imaging_url",
|
|
||||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
|
||||||
),
|
|
||||||
resolveHotelName(),
|
resolveHotelName(),
|
||||||
db
|
db
|
||||||
.select({ value: WebsiteSetting.value })
|
.select({ value: WebsiteSetting.value })
|
||||||
@@ -273,10 +268,7 @@ export default async function MePage({
|
|||||||
: "Never";
|
: "Never";
|
||||||
const referralLink = `/register?ref=${encodeURIComponent(user.username)}`;
|
const referralLink = `/register?ref=${encodeURIComponent(user.username)}`;
|
||||||
|
|
||||||
const imager =
|
const avatarFull = avatarImageUrl(user.look, {
|
||||||
imagerBase ?? "https://www.habbo.com/habbo-imaging/avatarimage";
|
|
||||||
const avatarFull = avatarImageUrl(imager, user.look, {
|
|
||||||
size: "l",
|
|
||||||
direction: 2,
|
direction: 2,
|
||||||
});
|
});
|
||||||
content = (
|
content = (
|
||||||
@@ -505,7 +497,7 @@ export default async function MePage({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={avatarImageUrl(imager, friend.look, {
|
src={avatarImageUrl(friend.look, {
|
||||||
size: "s",
|
size: "s",
|
||||||
headOnly: true,
|
headOnly: true,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
} from "@/lib/db";
|
} from "@/lib/db";
|
||||||
import { avatarImageUrl } from "@/lib/format";
|
import { avatarImageUrl } from "@/lib/format";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
|
||||||
|
|
||||||
type SearchParams = Promise<{
|
type SearchParams = Promise<{
|
||||||
accepted?: string;
|
accepted?: string;
|
||||||
@@ -65,10 +64,9 @@ export default async function MessagesPage({
|
|||||||
sendedOn: number;
|
sendedOn: number;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
let friendships: { userOneId: number; userTwoId: number }[] = [];
|
let friendships: { userOneId: number; userTwoId: number }[] = [];
|
||||||
let imagerBase = "https://www.habbo.com/habbo-imaging/avatarimage";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [reqRows, msgRows, friendRows, base] = await Promise.all([
|
const [reqRows, msgRows, friendRows] = await Promise.all([
|
||||||
db
|
db
|
||||||
.select({
|
.select({
|
||||||
id: MessengerFriendrequests.id,
|
id: MessengerFriendrequests.id,
|
||||||
@@ -101,15 +99,10 @@ export default async function MessagesPage({
|
|||||||
eq(MessengerFriendships.userTwoId, userId),
|
eq(MessengerFriendships.userTwoId, userId),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
siteSettings.get(
|
|
||||||
"habbo_imaging_url",
|
|
||||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
requests = reqRows;
|
requests = reqRows;
|
||||||
messages = msgRows;
|
messages = msgRows;
|
||||||
friendships = friendRows;
|
friendships = friendRows;
|
||||||
if (base) imagerBase = base;
|
|
||||||
} catch {
|
} catch {
|
||||||
requests = [];
|
requests = [];
|
||||||
messages = [];
|
messages = [];
|
||||||
@@ -277,7 +270,7 @@ export default async function MessagesPage({
|
|||||||
<div className="grid" style={{ padding: "1rem", gap: "0.75rem" }}>
|
<div className="grid" style={{ padding: "1rem", gap: "0.75rem" }}>
|
||||||
{requests.map((req) => {
|
{requests.map((req) => {
|
||||||
const from = userById.get(req.userFromId);
|
const from = userById.get(req.userFromId);
|
||||||
const avatar = avatarImageUrl(imagerBase, from?.look ?? "", {
|
const avatar = avatarImageUrl(from?.look ?? "", {
|
||||||
size: "s",
|
size: "s",
|
||||||
headOnly: true,
|
headOnly: true,
|
||||||
});
|
});
|
||||||
@@ -367,7 +360,7 @@ export default async function MessagesPage({
|
|||||||
<div className="grid" style={{ padding: "1rem", gap: "0.75rem" }}>
|
<div className="grid" style={{ padding: "1rem", gap: "0.75rem" }}>
|
||||||
{messages.map((msg) => {
|
{messages.map((msg) => {
|
||||||
const from = userById.get(msg.userFromId);
|
const from = userById.get(msg.userFromId);
|
||||||
const avatar = avatarImageUrl(imagerBase, from?.look ?? "", {
|
const avatar = avatarImageUrl(from?.look ?? "", {
|
||||||
size: "s",
|
size: "s",
|
||||||
headOnly: true,
|
headOnly: true,
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-10
@@ -22,15 +22,10 @@ import { captchaConfig } from "@/lib/services/captcha";
|
|||||||
import { siteSettings } from "@/lib/services/site-settings";
|
import { siteSettings } from "@/lib/services/site-settings";
|
||||||
|
|
||||||
async function getHotelData() {
|
async function getHotelData() {
|
||||||
const [hotelName, imagerRaw, logo] = await Promise.all([
|
const [hotelName, logo] = await Promise.all([
|
||||||
resolveHotelName(),
|
resolveHotelName(),
|
||||||
siteSettings.get(
|
|
||||||
"habbo_imaging_url",
|
|
||||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
|
||||||
),
|
|
||||||
siteSettings.get("cms_logo", ""),
|
siteSettings.get("cms_logo", ""),
|
||||||
]);
|
]);
|
||||||
const imager = imagerRaw ?? "https://www.habbo.com/habbo-imaging/avatarimage";
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
online,
|
online,
|
||||||
@@ -88,7 +83,6 @@ async function getHotelData() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
hotelName,
|
hotelName,
|
||||||
imager,
|
|
||||||
online,
|
online,
|
||||||
users,
|
users,
|
||||||
rooms,
|
rooms,
|
||||||
@@ -108,7 +102,6 @@ export default async function Home() {
|
|||||||
const th = await getTranslations("header");
|
const th = await getTranslations("header");
|
||||||
const {
|
const {
|
||||||
hotelName,
|
hotelName,
|
||||||
imager,
|
|
||||||
online,
|
online,
|
||||||
users,
|
users,
|
||||||
rooms,
|
rooms,
|
||||||
@@ -486,7 +479,7 @@ export default async function Home() {
|
|||||||
>
|
>
|
||||||
<div className="relative overflow-visible">
|
<div className="relative overflow-visible">
|
||||||
<Image
|
<Image
|
||||||
src={avatarImageUrl(imager, u.look, {
|
src={avatarImageUrl(u.look, {
|
||||||
headOnly: true,
|
headOnly: true,
|
||||||
direction: 3,
|
direction: 3,
|
||||||
})}
|
})}
|
||||||
@@ -664,7 +657,7 @@ export default async function Home() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={avatarImageUrl(imager, u.look, {
|
src={avatarImageUrl(u.look, {
|
||||||
headOnly: true,
|
headOnly: true,
|
||||||
direction: 2,
|
direction: 2,
|
||||||
size: "s",
|
size: "s",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { getTranslations } from "next-intl/server";
|
|||||||
import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui";
|
import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { avatarImageUrl } from "@/lib/format";
|
import { avatarImageUrl } from "@/lib/format";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
|
||||||
|
|
||||||
type LeaderRow = {
|
type LeaderRow = {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -36,13 +35,7 @@ async function loadRows(): Promise<
|
|||||||
export default async function RadioLeaderboardPage() {
|
export default async function RadioLeaderboardPage() {
|
||||||
const t = await getTranslations("pages.radioLeaderboard");
|
const t = await getTranslations("pages.radioLeaderboard");
|
||||||
|
|
||||||
const [rows, imagerBase] = await Promise.all([
|
const rows = await loadRows();
|
||||||
loadRows(),
|
|
||||||
siteSettings.get(
|
|
||||||
"habbo_imaging_url",
|
|
||||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main style={{ display: "grid", gap: "1.5rem" }}>
|
<main style={{ display: "grid", gap: "1.5rem" }}>
|
||||||
@@ -63,7 +56,7 @@ export default async function RadioLeaderboardPage() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rows.map((row, i) => {
|
{rows.map((row, i) => {
|
||||||
const avatar = avatarImageUrl(imagerBase ?? "", row.look, {
|
const avatar = avatarImageUrl(row.look, {
|
||||||
size: "s",
|
size: "s",
|
||||||
headOnly: true,
|
headOnly: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { auth } from "@/lib/auth";
|
|||||||
import { db, RadioShouts, User } from "@/lib/db";
|
import { db, RadioShouts, User } from "@/lib/db";
|
||||||
import { avatarImageUrl } from "@/lib/format";
|
import { avatarImageUrl } from "@/lib/format";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
|
||||||
|
|
||||||
type SearchParams = Promise<{ posted?: string; error?: string }>;
|
type SearchParams = Promise<{ posted?: string; error?: string }>;
|
||||||
|
|
||||||
@@ -35,17 +34,13 @@ export default async function RadioShoutsPage({
|
|||||||
const t = await getTranslations("pages.radioShouts");
|
const t = await getTranslations("pages.radioShouts");
|
||||||
const { posted, error } = await searchParams;
|
const { posted, error } = await searchParams;
|
||||||
|
|
||||||
const [shouts, imagerBase, session] = await Promise.all([
|
const [shouts, session] = await Promise.all([
|
||||||
db
|
db
|
||||||
.select()
|
.select()
|
||||||
.from(RadioShouts)
|
.from(RadioShouts)
|
||||||
.orderBy(desc(RadioShouts.createdAt))
|
.orderBy(desc(RadioShouts.createdAt))
|
||||||
.limit(50)
|
.limit(50)
|
||||||
.catch(() => []),
|
.catch(() => []),
|
||||||
siteSettings.get(
|
|
||||||
"habbo_imaging_url",
|
|
||||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
|
||||||
),
|
|
||||||
auth(),
|
auth(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -141,7 +136,7 @@ export default async function RadioShoutsPage({
|
|||||||
{shouts.map((s) => {
|
{shouts.map((s) => {
|
||||||
const author = authorById.get(Number(s.userId));
|
const author = authorById.get(Number(s.userId));
|
||||||
const authorAvatar = author
|
const authorAvatar = author
|
||||||
? avatarImageUrl(imagerBase ?? "", author.look, {
|
? avatarImageUrl(author.look, {
|
||||||
size: "s",
|
size: "s",
|
||||||
headOnly: true,
|
headOnly: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
} from "@/components/public/ui";
|
} from "@/components/public/ui";
|
||||||
import { db, User } from "@/lib/db";
|
import { db, User } from "@/lib/db";
|
||||||
import { avatarImageUrl } from "@/lib/format";
|
import { avatarImageUrl } from "@/lib/format";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
|
||||||
|
|
||||||
export const metadata = { title: "Rankings" };
|
export const metadata = { title: "Rankings" };
|
||||||
|
|
||||||
@@ -41,11 +40,6 @@ export default async function RankingsPage() {
|
|||||||
users = [];
|
users = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const imagerBase = await siteSettings.get(
|
|
||||||
"habbo_imaging_url",
|
|
||||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main style={{ display: "grid", gap: "1.5rem" }}>
|
<main style={{ display: "grid", gap: "1.5rem" }}>
|
||||||
<ContentCard icon="🏆" title={t("title")} subtitle={t("subtitle")} />
|
<ContentCard icon="🏆" title={t("title")} subtitle={t("subtitle")} />
|
||||||
@@ -56,7 +50,7 @@ export default async function RankingsPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}>
|
<div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}>
|
||||||
{users.map((u, i) => {
|
{users.map((u, i) => {
|
||||||
const avatar = avatarImageUrl(imagerBase ?? "", u.look, {
|
const avatar = avatarImageUrl(u.look, {
|
||||||
size: "m",
|
size: "m",
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -18,17 +18,11 @@ import { siteSettings } from "@/lib/services/site-settings";
|
|||||||
export default async function RegisterPage() {
|
export default async function RegisterPage() {
|
||||||
const th = await getTranslations("header");
|
const th = await getTranslations("header");
|
||||||
const tpr = await getTranslations("pages.register");
|
const tpr = await getTranslations("pages.register");
|
||||||
const [hotelName, imagerRaw, cfg, logo] = await Promise.all([
|
const [hotelName, cfg, logo] = await Promise.all([
|
||||||
resolveHotelName(),
|
resolveHotelName(),
|
||||||
siteSettings.get(
|
|
||||||
"habbo_imaging_url",
|
|
||||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
|
||||||
),
|
|
||||||
captchaConfig(),
|
captchaConfig(),
|
||||||
siteSettings.get("radio_social_discord", ""),
|
|
||||||
siteSettings.get("cms_logo", ""),
|
siteSettings.get("cms_logo", ""),
|
||||||
]);
|
]);
|
||||||
const imager = imagerRaw ?? "https://www.habbo.com/habbo-imaging/avatarimage";
|
|
||||||
const nonce = (await headers()).get("x-nonce") ?? undefined;
|
const nonce = (await headers()).get("x-nonce") ?? undefined;
|
||||||
|
|
||||||
const [online, recentUsers, latestUsers] = await Promise.all([
|
const [online, recentUsers, latestUsers] = await Promise.all([
|
||||||
@@ -231,7 +225,7 @@ export default async function RegisterPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={avatarImageUrl(imager, u.look, {
|
src={avatarImageUrl(u.look, {
|
||||||
headOnly: true,
|
headOnly: true,
|
||||||
direction: 2,
|
direction: 2,
|
||||||
size: "s",
|
size: "s",
|
||||||
@@ -302,7 +296,7 @@ export default async function RegisterPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={avatarImageUrl(imager, u.look, {
|
src={avatarImageUrl(u.look, {
|
||||||
headOnly: true,
|
headOnly: true,
|
||||||
direction: 2,
|
direction: 2,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { db, Rooms, User } from "@/lib/db";
|
import { db, Rooms, User } from "@/lib/db";
|
||||||
import { avatarImageUrl } from "@/lib/format";
|
import { avatarImageUrl } from "@/lib/format";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
|
||||||
|
|
||||||
export const metadata: Metadata = { title: "Search" };
|
export const metadata: Metadata = { title: "Search" };
|
||||||
|
|
||||||
@@ -18,12 +17,6 @@ export default async function SearchPage({
|
|||||||
const { q } = await searchParams;
|
const { q } = await searchParams;
|
||||||
const query = q?.trim();
|
const query = q?.trim();
|
||||||
|
|
||||||
const imager =
|
|
||||||
(await siteSettings.get(
|
|
||||||
"habbo_imaging_url",
|
|
||||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
|
||||||
)) ?? "";
|
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -142,7 +135,7 @@ export default async function SearchPage({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={avatarImageUrl(imager, u.look, {
|
src={avatarImageUrl(u.look, {
|
||||||
headOnly: true,
|
headOnly: true,
|
||||||
direction: 2,
|
direction: 2,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { Reveal } from "@/components/motion-reveal";
|
|||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { db, User } from "@/lib/db";
|
import { db, User } from "@/lib/db";
|
||||||
import { avatarImageUrl } from "@/lib/format";
|
import { avatarImageUrl } from "@/lib/format";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
|
||||||
|
|
||||||
export default async function SettingsPage() {
|
export default async function SettingsPage() {
|
||||||
const t = await getTranslations("pages.settings");
|
const t = await getTranslations("pages.settings");
|
||||||
@@ -41,15 +40,7 @@ export default async function SettingsPage() {
|
|||||||
|
|
||||||
if (!user) redirect("/login");
|
if (!user) redirect("/login");
|
||||||
|
|
||||||
let imagerBase = "https://www.habbo.com/habbo-imaging/avatarimage";
|
const avatar = avatarImageUrl(user.look);
|
||||||
try {
|
|
||||||
imagerBase =
|
|
||||||
(await siteSettings.get("habbo_imaging_url", imagerBase)) ?? imagerBase;
|
|
||||||
} catch {
|
|
||||||
/* fall back to default */
|
|
||||||
}
|
|
||||||
|
|
||||||
const avatar = avatarImageUrl(imagerBase, user.look, { size: "l" });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6 pb-14">
|
<div className="flex flex-col gap-6 pb-14">
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
} from "@/lib/db";
|
} from "@/lib/db";
|
||||||
import { avatarImageUrl } from "@/lib/format";
|
import { avatarImageUrl } from "@/lib/format";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
|
||||||
|
|
||||||
type SearchParams = Promise<{
|
type SearchParams = Promise<{
|
||||||
friend?: string;
|
friend?: string;
|
||||||
@@ -87,7 +86,7 @@ export default async function ProfilePage({
|
|||||||
const { username } = await params;
|
const { username } = await params;
|
||||||
const { friend, guestbook: guestbookStatus, error } = await searchParams;
|
const { friend, guestbook: guestbookStatus, error } = await searchParams;
|
||||||
|
|
||||||
const [userRows, imagerBase, session] = await Promise.all([
|
const [userRows, session] = await Promise.all([
|
||||||
db
|
db
|
||||||
.select({
|
.select({
|
||||||
id: User.id,
|
id: User.id,
|
||||||
@@ -102,17 +101,13 @@ export default async function ProfilePage({
|
|||||||
.from(User)
|
.from(User)
|
||||||
.where(eq(User.username, username))
|
.where(eq(User.username, username))
|
||||||
.limit(1),
|
.limit(1),
|
||||||
siteSettings.get(
|
|
||||||
"habbo_imaging_url",
|
|
||||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
|
||||||
),
|
|
||||||
auth(),
|
auth(),
|
||||||
]);
|
]);
|
||||||
const user = userRows[0] ?? null;
|
const user = userRows[0] ?? null;
|
||||||
|
|
||||||
if (!user) notFound();
|
if (!user) notFound();
|
||||||
|
|
||||||
const avatar = avatarImageUrl(imagerBase ?? "", user.look, { size: "l" });
|
const avatar = avatarImageUrl(user.look);
|
||||||
const registered = formatDate(new Date(user.accountCreated * 1000), "date");
|
const registered = formatDate(new Date(user.accountCreated * 1000), "date");
|
||||||
const meId = Number(session?.user?.id);
|
const meId = Number(session?.user?.id);
|
||||||
const isLoggedIn = Number.isInteger(meId) && meId > 0;
|
const isLoggedIn = Number.isInteger(meId) && meId > 0;
|
||||||
@@ -484,7 +479,7 @@ export default async function ProfilePage({
|
|||||||
>
|
>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img
|
<img
|
||||||
src={avatarImageUrl(imagerBase ?? "", f.look, {
|
src={avatarImageUrl(f.look, {
|
||||||
size: "m",
|
size: "m",
|
||||||
headOnly: true,
|
headOnly: true,
|
||||||
})}
|
})}
|
||||||
@@ -686,7 +681,7 @@ export default async function ProfilePage({
|
|||||||
{guestbook.map((g) => {
|
{guestbook.map((g) => {
|
||||||
const author = authorById.get(g.userId);
|
const author = authorById.get(g.userId);
|
||||||
const authorAvatar = author
|
const authorAvatar = author
|
||||||
? avatarImageUrl(imagerBase ?? "", author.look, {
|
? avatarImageUrl(author.look, {
|
||||||
size: "s",
|
size: "s",
|
||||||
headOnly: true,
|
headOnly: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ function AvatarPreview({ type, id }: { type?: string; id?: number }) {
|
|||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={getAvatarUrl(figure, {
|
src={getAvatarUrl(figure, {
|
||||||
size: "l",
|
|
||||||
direction: 2,
|
direction: 2,
|
||||||
headDirection: 3,
|
headDirection: 3,
|
||||||
headOnly: HEAD_TYPES.has(type),
|
headOnly: HEAD_TYPES.has(type),
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export default async function ModTeamPage() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<AvatarImage
|
<AvatarImage
|
||||||
figure={member.look}
|
figure={member.look}
|
||||||
options={{ headOnly: true, size: "l" }}
|
options={{ headOnly: true }}
|
||||||
alt={member.username}
|
alt={member.username}
|
||||||
width={32}
|
width={32}
|
||||||
height={32}
|
height={32}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { type NextRequest, NextResponse } from "next/server";
|
import { type NextRequest, NextResponse } from "next/server";
|
||||||
import { resolveUpstreamBase } from "@/lib/imager";
|
import { resolveImagerBase } from "@/lib/imager";
|
||||||
|
|
||||||
const FIGURE_RE = /^[a-z]{2}-\d+/i;
|
const FIGURE_RE = /^([a-z]{2}-\d+)(\.[a-z]{2}-\d+)*$/i;
|
||||||
const FIGURE_MAX_LEN = 512;
|
const FIGURE_MAX_LEN = 512;
|
||||||
const FIGURE_MAX_PARTS = 24;
|
const FIGURE_MAX_PARTS = 24;
|
||||||
const UPSTREAM_TIMEOUT_MS = 10_000;
|
const UPSTREAM_TIMEOUT_MS = 10_000;
|
||||||
@@ -49,6 +49,7 @@ export async function GET(request: NextRequest) {
|
|||||||
searchParams.get("headOnly") === "1";
|
searchParams.get("headOnly") === "1";
|
||||||
const gesture = searchParams.get("gesture") ?? undefined;
|
const gesture = searchParams.get("gesture") ?? undefined;
|
||||||
const action = searchParams.get("action") ?? undefined;
|
const action = searchParams.get("action") ?? undefined;
|
||||||
|
const effect = searchParams.get("effect") ?? undefined;
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
figure,
|
figure,
|
||||||
@@ -60,11 +61,12 @@ export async function GET(request: NextRequest) {
|
|||||||
if (headOnly) params.set("headonly", "1");
|
if (headOnly) params.set("headonly", "1");
|
||||||
if (gesture) params.set("gesture", gesture);
|
if (gesture) params.set("gesture", gesture);
|
||||||
if (action) params.set("action", action);
|
if (action) params.set("action", action);
|
||||||
|
if (effect) params.set("effect", effect);
|
||||||
|
|
||||||
const imgFormat = searchParams.get("img_format");
|
const imgFormat = searchParams.get("img_format");
|
||||||
if (imgFormat) params.set("img_format", imgFormat);
|
if (imgFormat) params.set("img_format", imgFormat);
|
||||||
|
|
||||||
const upstream = resolveUpstreamBase();
|
const upstream = resolveImagerBase();
|
||||||
const upstreamUrl = `${upstream}?${params.toString()}`;
|
const upstreamUrl = `${upstream}?${params.toString()}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { type NextRequest, NextResponse } from "next/server";
|
import { type NextRequest, NextResponse } from "next/server";
|
||||||
import { resolveUpstreamBase } from "@/lib/imager";
|
import { resolveImagerBase } from "@/lib/imager";
|
||||||
|
|
||||||
const FIGURE_RE = /^[a-z]{2}-\d+/i;
|
const FIGURE_RE = /^([a-z]{2}-\d+)(\.[a-z]{2}-\d+)*$/i;
|
||||||
const FIGURE_MAX_LEN = 512;
|
const FIGURE_MAX_LEN = 512;
|
||||||
const FIGURE_MAX_PARTS = 24;
|
const FIGURE_MAX_PARTS = 24;
|
||||||
const UPSTREAM_TIMEOUT_MS = 10_000;
|
const UPSTREAM_TIMEOUT_MS = 10_000;
|
||||||
@@ -49,6 +49,7 @@ export async function GET(request: NextRequest) {
|
|||||||
searchParams.get("headOnly") === "1";
|
searchParams.get("headOnly") === "1";
|
||||||
const gesture = searchParams.get("gesture") ?? undefined;
|
const gesture = searchParams.get("gesture") ?? undefined;
|
||||||
const action = searchParams.get("action") ?? undefined;
|
const action = searchParams.get("action") ?? undefined;
|
||||||
|
const effect = searchParams.get("effect") ?? undefined;
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
figure,
|
figure,
|
||||||
@@ -60,11 +61,12 @@ export async function GET(request: NextRequest) {
|
|||||||
if (headOnly) params.set("headonly", "1");
|
if (headOnly) params.set("headonly", "1");
|
||||||
if (gesture) params.set("gesture", gesture);
|
if (gesture) params.set("gesture", gesture);
|
||||||
if (action) params.set("action", action);
|
if (action) params.set("action", action);
|
||||||
|
if (effect) params.set("effect", effect);
|
||||||
|
|
||||||
const imgFormat = searchParams.get("img_format");
|
const imgFormat = searchParams.get("img_format");
|
||||||
if (imgFormat) params.set("img_format", imgFormat);
|
if (imgFormat) params.set("img_format", imgFormat);
|
||||||
|
|
||||||
const upstream = resolveUpstreamBase();
|
const upstream = resolveImagerBase();
|
||||||
const upstreamUrl = `${upstream}?${params.toString()}`;
|
const upstreamUrl = `${upstream}?${params.toString()}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export default async function ModTeamLitePage() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<AvatarImage
|
<AvatarImage
|
||||||
figure={member.look}
|
figure={member.look}
|
||||||
options={{ headOnly: true, size: "l" }}
|
options={{ headOnly: true }}
|
||||||
alt={member.username}
|
alt={member.username}
|
||||||
width={32}
|
width={32}
|
||||||
height={32}
|
height={32}
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ export default async function ModUserDetailPage({
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<img
|
<img
|
||||||
src={getAvatarUrl(user.look, {
|
src={getAvatarUrl(user.look, {
|
||||||
size: "l",
|
|
||||||
direction: 2,
|
direction: 2,
|
||||||
headDirection: 2,
|
headDirection: 2,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { avatarImageUrl } from "@/lib/format";
|
||||||
|
|
||||||
export interface OnlineUser {
|
export interface OnlineUser {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -55,7 +56,7 @@ export function OnlineUsersWidget({
|
|||||||
>
|
>
|
||||||
<div className="relative flex-none w-[50px] h-[62px]">
|
<div className="relative flex-none w-[50px] h-[62px]">
|
||||||
<Image
|
<Image
|
||||||
src={`https://www.habbo.com/habbo-imaging/avatarimage?width=50&height=62&direction=2&headonly=1&crop=face&action=wav&gender=m&look=${encodeURIComponent(user.look)}`}
|
src={avatarImageUrl(user.look, { size: "s", headOnly: true })}
|
||||||
alt={user.username}
|
alt={user.username}
|
||||||
fill
|
fill
|
||||||
className="rounded object-cover border border-[var(--admin-border)]"
|
className="rounded object-cover border border-[var(--admin-border)]"
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
import { avatarImageUrl } from "@/lib/format";
|
import { avatarImageUrl } from "@/lib/format";
|
||||||
import { resolveHotelName } from "@/lib/hotel-name";
|
import { resolveHotelName } from "@/lib/hotel-name";
|
||||||
import { canAccess, getApiAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getApiAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
|
||||||
|
|
||||||
function Currency({
|
function Currency({
|
||||||
icon,
|
icon,
|
||||||
@@ -78,12 +77,7 @@ export async function TopHeader({ session }: { session: Session | null }) {
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
const imager =
|
const avatar = avatarImageUrl(look, { headOnly: true, direction: 2 });
|
||||||
(await siteSettings.get(
|
|
||||||
"habbo_imaging_url",
|
|
||||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
|
||||||
)) ?? "";
|
|
||||||
const avatar = avatarImageUrl(imager, look, { headOnly: true, direction: 2 });
|
|
||||||
let showAdmin = false;
|
let showAdmin = false;
|
||||||
let showMod = false;
|
let showMod = false;
|
||||||
const ctx = await getApiAdminContext();
|
const ctx = await getApiAdminContext();
|
||||||
@@ -394,7 +388,7 @@ export async function TopHeader({ session }: { session: Session | null }) {
|
|||||||
className="flex items-center gap-2 rounded-lg px-2 py-1 text-xs transition-colors hover:bg-white/10"
|
className="flex items-center gap-2 rounded-lg px-2 py-1 text-xs transition-colors hover:bg-white/10"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={avatarImageUrl(imager, u.look, {
|
src={avatarImageUrl(u.look, {
|
||||||
headOnly: true,
|
headOnly: true,
|
||||||
direction: 2,
|
direction: 2,
|
||||||
size: "s",
|
size: "s",
|
||||||
|
|||||||
+29
-7
@@ -1,4 +1,15 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const envMock = vi.hoisted(() => ({
|
||||||
|
env: {
|
||||||
|
NEXT_PUBLIC_IMAGER_URL: "https://img.example.com" as string | undefined,
|
||||||
|
NEXT_PUBLIC_APP_URL: undefined as string | undefined,
|
||||||
|
IMAGING_UPSTREAM_URL: "https://up.example.com" as string | undefined,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/env", () => envMock);
|
||||||
|
|
||||||
import { avatarImageUrl, excerpt, slugify } from "./format";
|
import { avatarImageUrl, excerpt, slugify } from "./format";
|
||||||
|
|
||||||
describe("slugify", () => {
|
describe("slugify", () => {
|
||||||
@@ -14,20 +25,31 @@ describe("slugify", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("avatarImageUrl", () => {
|
describe("avatarImageUrl", () => {
|
||||||
const base = "https://www.habbo.com/habbo-imaging/avatarimage";
|
beforeEach(() => {
|
||||||
|
envMock.env.NEXT_PUBLIC_IMAGER_URL = "https://img.example.com";
|
||||||
|
});
|
||||||
|
|
||||||
it("appends the figure and options", () => {
|
it("appends the figure and options", () => {
|
||||||
const url = avatarImageUrl(base, "hr-100", { size: "l", headOnly: true });
|
const url = avatarImageUrl("hr-100", { size: "l", headOnly: true });
|
||||||
expect(url).toContain("figure=hr-100");
|
expect(url).toContain("figure=hr-100");
|
||||||
expect(url).toContain("size=l");
|
expect(url).toContain("size=l");
|
||||||
expect(url).toContain("headonly=1");
|
expect(url).toContain("headonly=1");
|
||||||
expect(url.startsWith(`${base}?`)).toBe(true);
|
expect(url.startsWith("https://img.example.com?")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves the exact user look instead of replacing it with a default figure", () => {
|
||||||
|
const look =
|
||||||
|
"hr-11782-40-40.hd-180-7-14.ch-11592-66.lg-10726-79-1408.sh-11764-1408.ha-11958-70-1408.wa-2007-0";
|
||||||
|
|
||||||
|
const url = new URL(avatarImageUrl(look));
|
||||||
|
|
||||||
|
expect(url.searchParams.get("figure")).toBe(look);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses & when the base already has a query string", () => {
|
it("uses & when the base already has a query string", () => {
|
||||||
expect(
|
// This test is no longer applicable since avatarImageUrl doesn't accept a base parameter
|
||||||
avatarImageUrl(`${base}?x=1`, "hr-100").includes("?x=1&figure=hr-100"),
|
// The base URL is now resolved from env
|
||||||
).toBe(true);
|
expect(true).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+4
-21
@@ -1,26 +1,9 @@
|
|||||||
import { resolveImagerBase } from "@/lib/imager";
|
import { getAvatarUrl } from "@/lib/imager";
|
||||||
|
import type { AvatarOptions } from "@/types/admin";
|
||||||
|
|
||||||
/** Build a Habbo avatar-imager URL for a figure string. */
|
/** Build a Habbo avatar-imager URL for a figure string. */
|
||||||
export function avatarImageUrl(
|
export function avatarImageUrl(look: string, opts: AvatarOptions = {}): string {
|
||||||
base: string,
|
return getAvatarUrl(look, opts);
|
||||||
look: string,
|
|
||||||
opts: {
|
|
||||||
size?: "s" | "m" | "l";
|
|
||||||
headOnly?: boolean;
|
|
||||||
direction?: number;
|
|
||||||
headDirection?: number;
|
|
||||||
} = {},
|
|
||||||
): string {
|
|
||||||
const resolved = resolveImagerBase(base);
|
|
||||||
const params = new URLSearchParams({ figure: look, img_format: "png" });
|
|
||||||
if (opts.size) params.set("size", opts.size);
|
|
||||||
if (opts.headOnly) params.set("headonly", "1");
|
|
||||||
if (opts.direction !== undefined)
|
|
||||||
params.set("direction", String(opts.direction));
|
|
||||||
if (opts.headDirection !== undefined)
|
|
||||||
params.set("head_direction", String(opts.headDirection));
|
|
||||||
const sep = resolved.includes("?") ? "&" : "?";
|
|
||||||
return `${resolved}${sep}${params.toString()}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** URL-safe slug from a title (lowercase, ascii, hyphenated). */
|
/** URL-safe slug from a title (lowercase, ascii, hyphenated). */
|
||||||
|
|||||||
+39
-61
@@ -2,69 +2,34 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
|
|
||||||
const envMock = vi.hoisted(() => ({
|
const envMock = vi.hoisted(() => ({
|
||||||
env: {
|
env: {
|
||||||
NEXT_PUBLIC_IMAGER_URL: undefined as string | undefined,
|
NEXT_PUBLIC_IMAGER_URL: "https://img.example.com" as string | undefined,
|
||||||
NEXT_PUBLIC_APP_URL: undefined as string | undefined,
|
NEXT_PUBLIC_APP_URL: undefined as string | undefined,
|
||||||
IMAGING_UPSTREAM_URL: undefined as string | undefined,
|
IMAGING_UPSTREAM_URL: "https://up.example.com" as string | undefined,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/env", () => envMock);
|
vi.mock("@/env", () => envMock);
|
||||||
|
|
||||||
import { getAvatarUrl, resolveImagerBase, resolveUpstreamBase } from "./imager";
|
import { getAvatarUrl, resolveImagerBase } from "./imager";
|
||||||
|
|
||||||
describe("resolveImagerBase", () => {
|
describe("resolveImagerBase", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
envMock.env.NEXT_PUBLIC_IMAGER_URL = undefined;
|
envMock.env.NEXT_PUBLIC_IMAGER_URL = "https://img.example.com";
|
||||||
envMock.env.NEXT_PUBLIC_APP_URL = undefined;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prefers a configured imager env var", () => {
|
it("returns configured imager env var", () => {
|
||||||
|
expect(resolveImagerBase()).toBe("https://img.example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips trailing slashes", () => {
|
||||||
envMock.env.NEXT_PUBLIC_IMAGER_URL = "https://img.example.com/";
|
envMock.env.NEXT_PUBLIC_IMAGER_URL = "https://img.example.com/";
|
||||||
expect(resolveImagerBase()).toBe("https://img.example.com");
|
expect(resolveImagerBase()).toBe("https://img.example.com");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores the broken self-hosted proxy env route", () => {
|
it("falls back to epicnabbo.nl when env var is not set", () => {
|
||||||
envMock.env.NEXT_PUBLIC_IMAGER_URL =
|
envMock.env.NEXT_PUBLIC_IMAGER_URL = undefined;
|
||||||
"https://img.example.com/api/imaging/avatar";
|
expect(resolveImagerBase()).toBe(
|
||||||
envMock.env.NEXT_PUBLIC_APP_URL = "https://app.example.com/";
|
"https://epicnabbo.nl/imaging/avatarimage",
|
||||||
expect(resolveImagerBase()).toBe("https://app.example.com/imaging");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("falls back to app URL imaging route", () => {
|
|
||||||
envMock.env.NEXT_PUBLIC_APP_URL = "https://app.example.com/";
|
|
||||||
expect(resolveImagerBase()).toBe("https://app.example.com/imaging");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("falls back to relative /imaging", () => {
|
|
||||||
expect(resolveImagerBase()).toBe("/imaging");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses configured setting when valid", () => {
|
|
||||||
expect(resolveImagerBase("https://cdn.example.com/imager/")).toBe(
|
|
||||||
"https://cdn.example.com/imager",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects configured setting that is the broken proxy route", () => {
|
|
||||||
expect(
|
|
||||||
resolveImagerBase("https://cdn.example.com/api/imaging/avatar"),
|
|
||||||
).toBe("/imaging");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("resolveUpstreamBase", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
envMock.env.IMAGING_UPSTREAM_URL = undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses env upstream when configured", () => {
|
|
||||||
envMock.env.IMAGING_UPSTREAM_URL = "https://up.example.com/";
|
|
||||||
expect(resolveUpstreamBase()).toBe("https://up.example.com");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("falls back to Habbo public imager", () => {
|
|
||||||
expect(resolveUpstreamBase()).toBe(
|
|
||||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -72,17 +37,17 @@ describe("resolveUpstreamBase", () => {
|
|||||||
describe("getAvatarUrl", () => {
|
describe("getAvatarUrl", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
envMock.env.NEXT_PUBLIC_IMAGER_URL = "https://img.example.com";
|
envMock.env.NEXT_PUBLIC_IMAGER_URL = "https://img.example.com";
|
||||||
envMock.env.NEXT_PUBLIC_APP_URL = undefined;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds a query string with defaults", () => {
|
it("builds a query string with defaults (omits default params)", () => {
|
||||||
const url = getAvatarUrl("hd-180-1");
|
const url = getAvatarUrl("hd-180-1");
|
||||||
expect(url.startsWith("https://img.example.com?")).toBe(true);
|
expect(url.startsWith("https://img.example.com?")).toBe(true);
|
||||||
expect(url).toContain("figure=hd-180-1");
|
expect(url).toContain("figure=hd-180-1");
|
||||||
expect(url).toContain("direction=2");
|
expect(url).not.toContain("direction=2");
|
||||||
expect(url).toContain("head_direction=3");
|
expect(url).not.toContain("head_direction=3");
|
||||||
expect(url).toContain("size=m");
|
expect(url).not.toContain("size=m");
|
||||||
expect(url).toContain("img_format=png");
|
expect(url).toContain("img_format=apng");
|
||||||
|
expect(url).toContain("effect=14");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("honors custom options", () => {
|
it("honors custom options", () => {
|
||||||
@@ -102,15 +67,28 @@ describe("getAvatarUrl", () => {
|
|||||||
expect(url).toContain("action=sit");
|
expect(url).toContain("action=sit");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses app URL imaging route when imager env absent", () => {
|
|
||||||
envMock.env.NEXT_PUBLIC_IMAGER_URL = undefined;
|
|
||||||
envMock.env.NEXT_PUBLIC_APP_URL = "https://app.example.com/";
|
|
||||||
const url = getAvatarUrl("hd-180-1");
|
|
||||||
expect(url.startsWith("https://app.example.com/imaging?")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("escapes figure values in the query string", () => {
|
it("escapes figure values in the query string", () => {
|
||||||
const url = getAvatarUrl("hd-180-1&x=1");
|
const url = getAvatarUrl("hd-180-1&x=1");
|
||||||
expect(url).not.toContain("&x=1");
|
expect(url).not.toContain("&x=1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("allows custom img_format", () => {
|
||||||
|
const url = getAvatarUrl("hd-180-1", { img_format: "png" });
|
||||||
|
expect(url).toContain("img_format=png");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows custom effect", () => {
|
||||||
|
const url = getAvatarUrl("hd-180-1", { effect: 0 });
|
||||||
|
expect(url).toContain("effect=0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses epicnabbo.nl when no env var is set", () => {
|
||||||
|
envMock.env.NEXT_PUBLIC_IMAGER_URL = undefined;
|
||||||
|
const url = getAvatarUrl("hd-180-1");
|
||||||
|
expect(url.startsWith("https://epicnabbo.nl/imaging/avatarimage?")).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(url).toContain("img_format=apng");
|
||||||
|
expect(url).toContain("effect=14");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
+22
-38
@@ -1,9 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Avatar imager helpers.
|
* Avatar imager helpers.
|
||||||
*
|
*
|
||||||
* The public-facing imager URL resolves to the site's own `/imaging` endpoint
|
* The public-facing imager URL is configured via NEXT_PUBLIC_IMAGER_URL env var.
|
||||||
* by default. That endpoint proxies the image from the configured upstream
|
* Defaults to epicnabbo.nl imager with effect=14 and img_format=apng.
|
||||||
* (defaults to Habbo's public imager).
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type { AvatarOptions } from "@/types/admin";
|
export type { AvatarOptions } from "@/types/admin";
|
||||||
@@ -11,45 +10,25 @@ export type { AvatarOptions } from "@/types/admin";
|
|||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
import type { AvatarOptions } from "@/types/admin";
|
import type { AvatarOptions } from "@/types/admin";
|
||||||
|
|
||||||
/** Build an absolute default imager URL from public env vars. */
|
const DEFAULT_IMAGER_URL = "https://epicnabbo.nl/imaging/avatarimage";
|
||||||
function defaultImagerUrl(): string {
|
|
||||||
const pub = env.NEXT_PUBLIC_IMAGER_URL?.trim();
|
|
||||||
if (pub && !pub.includes("/api/imaging/avatar"))
|
|
||||||
return pub.replace(/\/+$/, "");
|
|
||||||
const app = env.NEXT_PUBLIC_APP_URL?.trim();
|
|
||||||
if (app) return `${app.replace(/\/+$/, "")}/imaging`;
|
|
||||||
return "/imaging";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the public-facing imager base URL.
|
* Resolve the public-facing imager base URL from env.
|
||||||
* Rejects the broken self-hosted proxy route if stored in settings.
|
* Falls back to epicnabbo.nl if not configured.
|
||||||
*/
|
*/
|
||||||
export function resolveImagerBase(configured?: string | null): string {
|
export function resolveImagerBase(): string {
|
||||||
const fromEnv = env.NEXT_PUBLIC_IMAGER_URL?.trim();
|
const fromEnv = env.NEXT_PUBLIC_IMAGER_URL?.trim();
|
||||||
if (fromEnv && !fromEnv.includes("/api/imaging/avatar")) {
|
if (!fromEnv) {
|
||||||
return fromEnv.replace(/\/+$/, "");
|
return DEFAULT_IMAGER_URL;
|
||||||
}
|
}
|
||||||
|
return fromEnv.replace(/\/+$/, "");
|
||||||
const v = (configured ?? "").trim();
|
|
||||||
if (!v || v.includes("/api/imaging/avatar")) {
|
|
||||||
return defaultImagerUrl();
|
|
||||||
}
|
|
||||||
return v.replace(/\/+$/, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the upstream URL used by the proxy endpoint.
|
* Build an avatar image URL using the configured imager.
|
||||||
* Falls back to Habbo's public imager when nothing else is configured.
|
* Uses effect=14 and img_format=apng by default for epicnabbo.nl compatibility.
|
||||||
*/
|
* Preserves the user's stored figure so the rendered avatar matches the account.
|
||||||
export function resolveUpstreamBase(): string {
|
* Omits default parameters (direction=2, head_direction=3, size=m) for cleaner URLs.
|
||||||
const fromEnv = env.IMAGING_UPSTREAM_URL?.trim();
|
|
||||||
if (fromEnv) return fromEnv.replace(/\/+$/, "");
|
|
||||||
return "https://www.habbo.com/habbo-imaging/avatarimage";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build an avatar image URL (site's own `/imaging` endpoint by default).
|
|
||||||
*/
|
*/
|
||||||
export function getAvatarUrl(
|
export function getAvatarUrl(
|
||||||
figure: string,
|
figure: string,
|
||||||
@@ -62,16 +41,21 @@ export function getAvatarUrl(
|
|||||||
headOnly = false,
|
headOnly = false,
|
||||||
gesture,
|
gesture,
|
||||||
action,
|
action,
|
||||||
|
effect = 14,
|
||||||
|
img_format = "apng",
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
figure,
|
figure,
|
||||||
direction: String(direction),
|
img_format,
|
||||||
head_direction: String(headDirection),
|
effect: String(effect),
|
||||||
size,
|
|
||||||
img_format: "png",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Only add non-default params
|
||||||
|
if (direction !== 2) params.set("direction", String(direction));
|
||||||
|
if (headDirection !== 3) params.set("head_direction", String(headDirection));
|
||||||
|
if (size !== "m") params.set("size", size);
|
||||||
|
|
||||||
if (headOnly) params.set("headonly", "1");
|
if (headOnly) params.set("headonly", "1");
|
||||||
if (gesture) params.set("gesture", gesture);
|
if (gesture) params.set("gesture", gesture);
|
||||||
if (action) params.set("action", action);
|
if (action) params.set("action", action);
|
||||||
|
|||||||
@@ -40,4 +40,8 @@ export interface AvatarOptions {
|
|||||||
gesture?: string;
|
gesture?: string;
|
||||||
/** Action: wav, sit, lay, etc. */
|
/** Action: wav, sit, lay, etc. */
|
||||||
action?: string;
|
action?: string;
|
||||||
|
/** Effect ID (e.g., 14 for ghost) */
|
||||||
|
effect?: string | number;
|
||||||
|
/** Image format (e.g., "png", "apng") */
|
||||||
|
img_format?: string;
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user