diff --git a/helpers/trading.ts b/helpers/trading.ts new file mode 100644 index 0000000..7b54b91 --- /dev/null +++ b/helpers/trading.ts @@ -0,0 +1,64 @@ +import TradingListing from "../models/tradingListing"; + +interface ListingData { + type: string; + team: number; + teamName?: string; + item: string; + category: string; + quantity?: number; + description?: string; + contact: string; + event?: string; +} + +interface ListingFilters { + type?: string; + category?: string; + event?: string; +} + +export async function addListing(data: ListingData) { + const listing = new TradingListing({ + type: data.type, + team: data.team, + teamName: data.teamName || "", + item: data.item, + category: data.category, + quantity: data.quantity || 1, + description: data.description || "", + contact: data.contact, + event: data.event || "general", + timestamp: new Date() + }); + await listing.save(); + return listing.toObject(); +} + +export async function getListings(filters?: ListingFilters) { + const query: any = {}; + if (filters) { + if (filters.type && filters.type !== "all") { + query.type = filters.type; + } + if (filters.category && filters.category !== "all") { + query.category = filters.category; + } + if (filters.event && filters.event !== "all") { + query.event = filters.event; + } + } + return TradingListing.find(query).sort({ timestamp: -1 }).lean(); +} + +export async function deleteListing(id: string, team: number) { + const listing: any = await TradingListing.findById(id); + if (!listing) { + throw new Error("Listing not found"); + } + if (listing.team !== team) { + throw new Error("You can only delete your own listings"); + } + await listing.deleteOne(); + return true; +} diff --git a/index.ts b/index.ts index bdc4a2a..711fc55 100644 --- a/index.ts +++ b/index.ts @@ -29,6 +29,7 @@ import resourcesAPIRouter from "./routers/api/resources"; import scoutingRouter from "./routers/scouting"; import scoutingAPIRouter from "./routers/api/scouting"; import tpsAPIRouter from "./routers/api/tps"; +import tradingAPIRouter from "./routers/trading"; const app = new Koa(); @@ -136,6 +137,7 @@ if (config.features.includes("scouting")) { if (config.features.includes("tps")) { router.use("/api/v1/tps", tpsAPIRouter.routes()); } +router.use("/api/v1/trading", tradingAPIRouter.routes()); function formatNumber(num) { if (num < 10) { diff --git a/models/tradingListing.ts b/models/tradingListing.ts new file mode 100644 index 0000000..7e82a6b --- /dev/null +++ b/models/tradingListing.ts @@ -0,0 +1,39 @@ +import mongoose from "../db"; + +export default mongoose.model( + "TradingListing", + new mongoose.Schema({ + type: { + type: String, + enum: ["offer", "request"], + required: true + }, + team: { + type: Number, + required: true + }, + teamName: String, + item: { + type: String, + required: true + }, + category: { + type: String, + required: true + }, + quantity: { + type: Number, + default: 1 + }, + description: String, + contact: { + type: String, + required: true + }, + event: String, + timestamp: { + type: Date, + default: Date.now + } + }) +); diff --git a/package-lock.json b/package-lock.json index 7749ad8..eb882dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -175,97 +175,6 @@ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "string-width-cjs": { - "version": "npm:string-width@4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "dependencies": { - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "strip-ansi-cjs": { - "version": "npm:strip-ansi@6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "wrap-ansi-cjs": { - "version": "npm:wrap-ansi@7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - } } }, "@jridgewell/resolve-uri": { @@ -4793,6 +4702,36 @@ "strip-ansi": "^7.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -4816,6 +4755,21 @@ "ansi-regex": "^6.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + } + } + }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -5203,6 +5157,67 @@ } } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/routers/trading.ts b/routers/trading.ts new file mode 100644 index 0000000..4a8f101 --- /dev/null +++ b/routers/trading.ts @@ -0,0 +1,130 @@ +import Koa from "koa"; +import Router from "koa-router"; +import bodyParser from "koa-bodyparser"; +import { getTeam } from "../helpers/tba"; +import { addListing, getListings, deleteListing } from "../helpers/trading"; +import { addAPIHeaders } from "../helpers/utils"; + +const router = new Router(); + +router.get("/team/:number", async (ctx) => { + addAPIHeaders(ctx); + try { + const teamNumber = parseInt(ctx.params.number); + if (!teamNumber || teamNumber < 1) { + ctx.body = { success: false, error: "Invalid team number" }; + return; + } + const team = await getTeam(teamNumber); + if (team && (team as any).nickname) { + ctx.body = { + success: true, + body: { + team: teamNumber, + teamName: (team as any).nickname + } + }; + } else { + ctx.body = { success: false, error: "Team not found" }; + } + } catch (err) { + ctx.body = { success: false, error: "Could not look up team" }; + } +}); + +router.get("/listings", async (ctx) => { + addAPIHeaders(ctx); + try { + const query = ctx.query as any; + const listings = await getListings({ + type: query.type, + category: query.category, + event: query.event + }); + ctx.body = { + success: true, + body: { listings } + }; + } catch (err) { + ctx.body = { + success: false, + error: "Could not fetch listings" + }; + } +}); + +router.post("/listings", bodyParser(), async (ctx) => { + addAPIHeaders(ctx); + try { + const body = ctx.request.body as any; + + if ( + !body.type || + !body.team || + !body.item || + !body.category || + !body.contact + ) { + ctx.body = { + success: false, + error: "Missing required fields: type, team, item, category, contact" + }; + return; + } + + if (!["offer", "request"].includes(body.type)) { + ctx.body = { + success: false, + error: "Type must be 'offer' or 'request'" + }; + return; + } + + const listing = await addListing({ + type: body.type, + team: parseInt(body.team), + teamName: body.teamName || "", + item: body.item, + category: body.category, + quantity: parseInt(body.quantity) || 1, + description: body.description || "", + contact: body.contact, + event: body.event || "general" + }); + + ctx.body = { + success: true, + body: { + listing + } + }; + } catch (err) { + ctx.body = { + success: false, + error: "Could not create listing" + }; + } +}); + +router.delete("/listings/:id", bodyParser(), async (ctx) => { + addAPIHeaders(ctx); + try { + const body = ctx.request.body as any; + if (!body.team) { + ctx.body = { + success: false, + error: "Team number required" + }; + return; + } + await deleteListing(ctx.params.id, parseInt(body.team)); + ctx.body = { success: true, body: { message: "Listing deleted" } }; + } catch (err) { + ctx.body = { + success: false, + error: err.message || "Could not delete listing" + }; + } +}); + +export default router; diff --git a/static/css/trading.css b/static/css/trading.css new file mode 100644 index 0000000..5d63f47 --- /dev/null +++ b/static/css/trading.css @@ -0,0 +1,967 @@ +:root { + --backgroundColor: #f8f0ff; + --contentColor: #404040; + --primaryBackgroundColor: #7d4090; + --primaryContentColor: #f4f4f4; + --primaryDarkerBackgroundColor: #5d2e6c; + --primaryDarkerBackgroundColor2: #6c378e; + --disabledColor: #747474; + --standard-border-radius: 7px; + --option-bg: white; + --surface-color: #ffffff; + --surface-dim: #eee; +} + +[data-theme="dark"] { + --backgroundColor: #1a1025; + --contentColor: #e0d6e8; + --primaryBackgroundColor: #5c2d6e; + --primaryContentColor: #d4c8de; + --primaryDarkerBackgroundColor: #3d1a4d; + --primaryDarkerBackgroundColor2: #4a2259; + --disabledColor: #888888; + --option-bg: #2a1d35; + --surface-color: #2a1d35; + --surface-dim: #332440; +} + +.trading-main input[type="number"]::-webkit-inner-spin-button, +.trading-main input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.trading-main input[type="number"] { + -moz-appearance: textfield; +} + +.trading-team-overlay { + display: none; + position: fixed; + inset: 0; + z-index: 2000; + background: rgba(0, 0, 0, 0.55); + align-items: center; + justify-content: center; +} + +.trading-team-overlay.active { + display: flex; +} + +.trading-team-prompt { + background: white; + border-radius: var(--standard-border-radius); + padding: 32px 28px; + max-width: 400px; + width: 90%; + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.25); + text-align: center; +} + +.trading-team-prompt h2 { + margin: 0 0 6px 0; + font-size: 1.3rem; + color: var(--contentColor); +} + +.trading-team-prompt > p { + margin: 0 0 20px 0; + font-size: 0.9rem; + color: var(--disabledColor); +} + +.trading-team-prompt-input { + display: flex; + gap: 10px; +} + +.trading-team-prompt-input input { + flex: 1; + padding: 10px 12px; + border: 2px solid #ccc; + border-radius: var(--standard-border-radius); + font-size: 15px; + outline: none; + transition: border-color 0.3s ease; +} + +.trading-team-prompt-input input:focus { + border-color: var(--primaryBackgroundColor); +} + +.trading-team-prompt-input .trading-btn-primary { + white-space: nowrap; + padding: 10px 20px; + font-size: 0.9rem; +} + +.trading-team-prompt-error { + color: #d32f2f; + font-size: 0.8rem; + margin: 10px 0 0 0; + min-height: 1.2em; +} + +.dark-mode-toggle .toggle-track { + width: 40px; + height: 22px; + background: rgba(255, 255, 255, 0.25); + border-radius: 11px; + position: relative; + transition: background 0.2s; + flex-shrink: 0; +} + +[data-theme="dark"] .dark-mode-toggle .toggle-track { + background: rgba(255, 255, 255, 0.45); +} + +.dark-mode-toggle .toggle-thumb { + width: 18px; + height: 18px; + background: white; + border-radius: 50%; + position: absolute; + top: 2px; + left: 2px; + transition: transform 0.2s; +} + +[data-theme="dark"] .dark-mode-toggle .toggle-thumb { + transform: translateX(18px); +} + +.hidden { + display: none; +} + +.trading-back-link { + color: white; + display: flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + text-decoration: none; +} + +.trading-back-link svg { + width: 100%; + height: 100%; +} + +.trading-main { + max-width: 900px; + margin: 0 auto; + padding: 20px 20px 40px 20px; +} + +.trading-section { + margin-bottom: 50px; +} + +.trading-section > h2 { + color: var(--contentColor); + font-size: 1.5rem; + margin: 0 0 5px 0; +} + +.trading-section > p { + color: var(--contentColor); + font-size: 0.95rem; + margin-bottom: 20px; +} + +.trading-title-block { + margin-bottom: 20px; +} + +.trading-title-block h1 { + color: var(--contentColor); + font-size: 2rem; + margin: 0; +} + +.trading-title-block h3 { + color: var(--contentColor); + font-size: 1.2rem; + font-weight: 500; + margin: 0; +} + +.trading-intro-text { + color: var(--contentColor); + font-size: 0.95rem; + line-height: 1.6; + margin-bottom: 10px; +} + +.trading-stats-row { + display: flex; + gap: 12px; + margin: 25px 0; + flex-wrap: wrap; +} + +.trading-stat-box { + flex: 1; + min-width: 80px; + background: white; + border-radius: var(--standard-border-radius); + padding: 16px 12px; + text-align: center; + box-shadow: 0 0 1em 0 rgba(0, 0, 0, 0.1); +} + +.trading-stat-num { + display: block; + font-size: 1.8rem; + font-weight: 700; + color: var(--primaryBackgroundColor); + line-height: 1.2; +} + +.trading-stat-label { + display: block; + font-size: 0.75rem; + color: var(--disabledColor); + letter-spacing: 0.5px; + text-transform: uppercase; + margin-top: 4px; +} + +.trading-hero-buttons { + display: flex; + gap: 10px; + margin-top: 20px; + flex-wrap: wrap; +} + +.trading-btn-primary { + background-color: var(--primaryBackgroundColor); + border: 2px solid var(--primaryBackgroundColor); + padding: 10px 30px; + font-size: 1rem; + color: var(--primaryContentColor); + border-radius: var(--standard-border-radius); + box-shadow: 0 0 1em 0 rgba(0, 0, 0, 0.2); + cursor: pointer; + transition: filter 0.2s ease-in-out; + font-family: sans-serif; + user-select: none; +} + +.trading-btn-primary:hover { + filter: brightness(1.25); +} + +.trading-btn-primary:active { + background-color: var(--primaryDarkerBackgroundColor); +} + +.trading-btn-primary:disabled { + opacity: 0.6; + cursor: not-allowed; + filter: none; +} + +.trading-btn-outline { + background-color: transparent; + border: 2px solid var(--primaryBackgroundColor); + padding: 10px 30px; + font-size: 1rem; + color: var(--primaryBackgroundColor); + border-radius: var(--standard-border-radius); + cursor: pointer; + transition: all 0.2s ease-in-out; + font-family: sans-serif; + user-select: none; + margin-left: 5px; +} + +.trading-btn-outline:hover { + background-color: var(--primaryBackgroundColor); + color: var(--primaryContentColor); +} + +.trading-filter-bar { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid rgba(0, 0, 0, 0.08); +} + +.trading-filter-tabs { + display: flex; + border-radius: var(--standard-border-radius); + overflow: hidden; + border: 2px solid var(--primaryBackgroundColor); +} + +.trading-tab { + background: transparent; + color: var(--contentColor); + border: none; + padding: 7px 18px; + font-size: 0.85rem; + font-family: sans-serif; + cursor: pointer; + transition: all 0.2s ease; +} + +.trading-tab:hover { + background-color: rgba(125, 64, 144, 0.08); +} + +.trading-tab.active { + background-color: var(--primaryBackgroundColor); + color: white; +} + +.trading-search-wrapper { + flex: 1; + min-width: 160px; +} + +.trading-search { + width: 100%; + padding: 8px 14px; + border: none; + border-bottom: 2px solid #ccc; + background: transparent; + color: var(--contentColor); + font-size: 0.9rem; + font-family: sans-serif; + outline: none; + box-sizing: border-box; + border-radius: 0; + transition: border-color 0.3s ease; +} + +.trading-search:focus { + border-bottom-color: var(--primaryBackgroundColor); +} + +.trading-search::placeholder { + color: var(--disabledColor); +} + +.trading-select { + padding: 8px 24px 8px 2px; + border: none; + border-bottom: 2px solid #ccc; + background-color: transparent; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23747474' d='M1.41 0L6 4.58 10.59 0 12 1.41l-6 6-6-6z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 4px center; + background-size: 10px 6px; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + font-size: 0.9rem; + font-family: sans-serif; + color: var(--contentColor); + cursor: pointer; + outline: none; + border-radius: 0; + transition: border-color 0.3s ease; + max-width: 100%; +} + +.trading-select:focus { + border-bottom-color: var(--primaryBackgroundColor); +} + +.trading-select option { + background-color: var(--option-bg); + color: var(--contentColor); + padding: 6px 10px; +} + +.trading-listings { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 15px; +} + +.trading-card { + background: white; + border-radius: var(--standard-border-radius); + padding: 18px; + box-shadow: 0 0 1em 0 rgba(0, 0, 0, 0.1); + transition: box-shadow 0.2s ease, transform 0.2s ease; + position: relative; + display: flex; + flex-direction: column; +} + +.trading-card:hover { + box-shadow: 0 0 1.5em 0 rgba(0, 0, 0, 0.18); + transform: translateY(-2px); +} + +.trading-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 8px; +} + +.trading-card-badge { + display: inline-block; + padding: 3px 10px; + border-radius: var(--standard-border-radius); + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 1px; + text-transform: uppercase; +} + +.trading-card-badge.offer { + background: rgba(76, 175, 80, 0.12); + color: #2e7d32; +} + +.trading-card-badge.request { + background: rgba(255, 152, 0, 0.12); + color: #e65100; +} + +.trading-card-category { + font-size: 0.7rem; + color: var(--disabledColor); + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.trading-card-title { + font-size: 1.1rem; + font-weight: 700; + color: var(--contentColor); + margin: 0 0 4px 0; + line-height: 1.3; +} + +.trading-card-team { + font-size: 0.85rem; + color: var(--primaryBackgroundColor); + margin: 0 0 8px 0; +} + +.trading-card-description { + font-size: 0.85rem; + color: var(--disabledColor); + line-height: 1.5; + margin: 0 0 12px 0; +} + +.trading-card-footer { + display: flex; + align-items: center; + margin-top: auto; + padding-top: 18px; + border-top: 1px solid rgba(0, 0, 0, 0.06); + gap: 10px; +} + +.trading-card-quantity { + font-size: 0.8rem; + color: var(--disabledColor); + margin-right: auto; +} + +.trading-card-quantity span { + color: var(--contentColor); + font-weight: 700; +} + +.trading-card-delete { + background: transparent; + border: none; + color: var(--disabledColor); + font-size: 0.85rem; + cursor: pointer; + padding: 4px 6px; + border-radius: 4px; + transition: all 0.2s ease; + font-family: sans-serif; + line-height: 1; +} + +.trading-card-delete:hover { + color: #d32f2f; + background: rgba(211, 47, 47, 0.08); +} + +.trading-card-contact { + padding: 4px 12px; + border: 2px solid var(--primaryBackgroundColor); + border-radius: var(--standard-border-radius); + color: var(--primaryBackgroundColor); + font-size: 0.75rem; + letter-spacing: 0.5px; + cursor: pointer; + transition: all 0.2s ease; + background: transparent; + font-family: sans-serif; + text-decoration: none; + white-space: nowrap; + line-height: 1; +} + +.trading-card-contact:hover { + background: var(--primaryBackgroundColor); + color: white; +} + +.trading-empty-state { + text-align: center; + padding: 40px 15px; + color: var(--disabledColor); +} + +.trading-empty-state p { + font-size: 0.95rem; + margin-bottom: 15px; +} + +.trading-form { + background: white; + border-radius: var(--standard-border-radius); + padding: 25px; + box-shadow: 0 0 1em 0 rgba(0, 0, 0, 0.1); + margin-top: 15px; +} + +.trading-team-fields { + display: flex; + gap: 16px; + align-items: flex-end; + margin-bottom: 28px; + flex-wrap: wrap; +} + +.trading-team-field { + flex: 1; + min-width: 120px; +} + +.trading-team-field label { + display: block; + font-size: 12px; + color: var(--primaryBackgroundColor); + margin-bottom: 6px; + font-weight: 600; +} + +.trading-team-field input[readonly] { + width: 100%; + padding: 8px 0; + border: none; + border-bottom: 2px solid #e0d4e8; + background: transparent; + font-size: 15px; + color: var(--contentColor); + cursor: default; + outline: none; + border-radius: 0; +} + +.trading-change-team { + display: none; + white-space: nowrap; + padding-bottom: 10px; +} + +.trading-change-team:hover { + text-decoration: underline; +} + +.trading-input-group { + position: relative; + width: 100%; + display: flex; + flex-direction: column; + margin-bottom: 28px; + padding-top: 16px; +} + +.trading-input-group input { + display: block; + width: 100%; + border: none; + border-bottom: 2px solid #ccc; + font-size: 16px; + background: transparent; + padding: 8px 0; + border-radius: 0; + color: var(--contentColor); + outline: none; + transition: border-color 0.3s ease; +} + +.trading-input-group input:focus { + border-bottom-color: var(--primaryBackgroundColor); +} + +.trading-input-group input:focus ~ .trading-bar::before { + width: 100%; +} + +.trading-input-group input:focus ~ label, +.trading-input-group input:valid ~ label { + top: 0; + font-size: 12px; + color: var(--primaryBackgroundColor); +} + +.trading-input-group label { + color: var(--disabledColor); + font-size: 16px; + font-weight: normal; + position: absolute; + pointer-events: none; + left: 0; + top: 24px; + transition: 300ms ease all; +} + +.trading-bar { + position: relative; + display: block; + width: 100%; +} + +.trading-bar::before { + content: ""; + height: 2px; + width: 0; + bottom: 0; + position: absolute; + background: var(--primaryBackgroundColor); + transition: 300ms ease all; + left: 0; +} + +.trading-form-group { + margin-bottom: 28px; +} + +.trading-form-label { + display: block; + font-size: 12px; + color: var(--primaryBackgroundColor); + margin-bottom: 8px; + font-weight: 600; +} + +.trading-form .trading-select { + width: 100%; + box-sizing: border-box; + padding: 8px 24px 8px 0; + font-size: 15px; +} + +.trading-textarea { + width: 100%; + padding: 8px 0; + border: none; + border-bottom: 2px solid #ccc; + background: transparent; + color: var(--contentColor); + font-size: 16px; + font-family: sans-serif; + outline: none; + resize: vertical; + box-sizing: border-box; + border-radius: 0; + min-height: 70px; + transition: border-color 0.3s ease; +} + +.trading-textarea:focus { + border-bottom-color: var(--primaryBackgroundColor); +} + +.trading-textarea::placeholder { + color: var(--disabledColor); +} + +.trading-type-toggle { + display: flex; + border-radius: var(--standard-border-radius); + overflow: hidden; + border: 2px solid var(--primaryBackgroundColor); +} + +.trading-type-btn { + flex: 1; + padding: 10px 16px; + border: none; + background: transparent; + color: var(--contentColor); + font-size: 0.9rem; + font-family: sans-serif; + cursor: pointer; + transition: all 0.2s ease; +} + +.trading-type-btn:hover { + background-color: rgba(125, 64, 144, 0.08); +} + +.trading-type-btn.active { + background: var(--primaryBackgroundColor); + color: white; +} + +.trading-submit { + width: 100%; + margin-top: 4px; +} + +.trading-success { + text-align: center; + padding: 30px 15px; +} + +.trading-success h3 { + color: var(--contentColor); + font-size: 1.2rem; + margin-bottom: 8px; +} + +.trading-success p { + color: var(--disabledColor); + font-size: 0.9rem; +} + +.trading-success .trading-btn-primary { + margin-top: 15px; + font-size: 0.85rem; + padding: 8px 20px; +} + +/* steps */ +.trading-steps { + display: flex; + gap: 20px; + margin-top: 20px; + flex-wrap: wrap; +} + +.trading-step { + flex: 1; + min-width: 180px; + background: white; + border-radius: var(--standard-border-radius); + padding: 20px; + box-shadow: 0 0 1em 0 rgba(0, 0, 0, 0.1); + text-align: center; +} + +.trading-step-num { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 50%; + background: var(--primaryBackgroundColor); + color: white; + font-size: 1.2rem; + font-weight: 700; + margin-bottom: 10px; +} + +.trading-step h3 { + color: var(--contentColor); + font-size: 1.1rem; + margin: 0 0 8px 0; +} + +.trading-step p { + color: var(--disabledColor); + font-size: 0.85rem; + line-height: 1.5; + margin: 0; +} + +/* footer */ +.trading-footer { + text-align: center; + padding: 30px 0 10px 0; + border-top: 1px solid rgba(0, 0, 0, 0.06); + margin-top: 20px; +} + +.trading-footer p { + color: var(--disabledColor); + font-size: 0.85rem; +} + +.trading-footer a { + color: var(--primaryBackgroundColor); + text-decoration: none; +} + +.trading-footer a:hover { + text-decoration: underline; +} + +/* Phone */ +@media (max-width: 600px) { + .trading-main { + padding: 15px 12px 30px 12px; + } + + .trading-filter-bar { + flex-direction: column; + align-items: stretch; + } + + .trading-filter-tabs { + align-self: flex-start; + } + + .trading-filter-bar .trading-select { + width: 100%; + } + + .trading-listings { + grid-template-columns: 1fr; + } + + .trading-stats-row { + gap: 8px; + } + + .trading-stat-box { + padding: 12px 8px; + } + + .trading-stat-num { + font-size: 1.4rem; + } + + .trading-steps { + flex-direction: column; + } + + .trading-hero-buttons { + flex-direction: column; + } + + .trading-hero-buttons button { + width: 100%; + margin-left: 0; + } + + .trading-form { + padding: 18px 15px; + } + + .trading-team-fields { + gap: 12px; + } + + .trading-team-prompt { + padding: 24px 20px; + } +} + +[data-theme="dark"] .trading-card, +[data-theme="dark"] .trading-stat-box, +[data-theme="dark"] .trading-step, +[data-theme="dark"] .trading-form, +[data-theme="dark"] .trading-team-prompt { + background-color: var(--surface-color); +} + +[data-theme="dark"] .trading-team-prompt-input input { + border-color: #555; + color: var(--primaryContentColor); + background: transparent; +} + +[data-theme="dark"] .trading-intro-text, +[data-theme="dark"] .trading-section > p { + color: #ccc; +} + +[data-theme="dark"] .trading-card-description, +[data-theme="dark"] .trading-card-quantity, +[data-theme="dark"] .trading-stat-label, +[data-theme="dark"] .trading-step p { + color: var(--primaryContentColor); +} + +[data-theme="dark"] .trading-card-footer { + border-top-color: rgba(255, 255, 255, 0.1); +} + +[data-theme="dark"] .trading-input-group input { + border-bottom-color: #555; + color: var(--contentColor); +} + +[data-theme="dark"] .trading-input-group label { + color: #999; +} + +[data-theme="dark"] .trading-form-label, +[data-theme="dark"] .trading-input-group input:valid ~ label, +[data-theme="dark"] .trading-input-group input:focus ~ label { + color: var(--primaryContentColor); +} + +[data-theme="dark"] .trading-textarea { + border-bottom-color: #555; + color: var(--contentColor); +} + +[data-theme="dark"] .trading-search { + border-bottom-color: #555; + color: var(--contentColor); +} + +[data-theme="dark"] .trading-select { + border-bottom-color: #555; + color: var(--contentColor); +} + +[data-theme="dark"] .trading-select option { + background-color: var(--surface-color); + color: var(--contentColor); +} + +[data-theme="dark"] .trading-filter-bar { + border-bottom-color: rgba(255, 255, 255, 0.1); +} + +[data-theme="dark"] .trading-footer { + border-top-color: rgba(255, 255, 255, 0.1); +} + +[data-theme="dark"] .trading-team-field input[readonly] { + border-bottom-color: #444; + color: #ccc; +} + +[data-theme="dark"] ::-webkit-scrollbar-thumb { + background: rgba(200, 150, 220, 0.3); +} + +[data-theme="dark"] ::-webkit-scrollbar-thumb:hover { + background: rgba(200, 150, 220, 0.5); +} + +[data-theme="dark"] .trading-card-team, +[data-theme="dark"] .trading-card-contact, +[data-theme="dark"] .trading-btn-outline { + color: var(--primaryContentColor); +} diff --git a/static/js/scoutingsdk.js b/static/js/scoutingsdk.js index dbb92b9..27a4243 100644 --- a/static/js/scoutingsdk.js +++ b/static/js/scoutingsdk.js @@ -6586,4 +6586,173 @@ ${_this.escape(teamNumber)} (Blue ${i + 1}) resolve(); }); }; + + _this.showBazaarPage = async () => { + return new Promise(async (resolve, reject) => { + try { + if (!document.getElementById("trading-css")) { + const link = document.createElement("link"); + link.id = "trading-css"; + link.rel = "stylesheet"; + link.href = "/css/trading.css"; + document.head.appendChild(link); + } + + if (!window.initBazaar) { + await new Promise((res, rej) => { + const script = document.createElement("script"); + script.src = "/js/trading.js"; + script.onload = res; + script.onerror = rej; + document.head.appendChild(script); + }); + } + + const data = await fetch( + "/api/v1/trading/team/" + + encodeURIComponent(config.account.team) + ).then((res) => res.json()); + const teamName = + data.success && data.body + ? data.body.teamName + : "Team " + config.account.team; + + element.innerHTML = ` +
+
+
+

Purple Bazaar

+

share parts, build together

+
+

+ Help other teams — or your own! — succeed at the competitions by sharing resources in the + spirit of coopertition. +

+

+ Purple Bazaar allows teams to volunteer or request resources from others. +

+
+
+ 0 + Offers +
+
+ 0 + Requests +
+
+ 0 + Teams +
+
+ 0 + Matched +
+
+ + +
+ +
+

Browse Listings

+
+
+ + + +
+
+ +
+ + +
+
+ +
+ +
+

Post a Listing

+

Whether you have spare parts to share or need something to compete, fill out the form to connect with other teams.

+
+
+ +
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + + +
+
+ + +
+
+ + + +
+
+ + +
+
+ + + +
+
+ + +
+ +
+
+ + +
`; + + window.initBazaar({ + team: { + team: parseInt(teamNumber), + teamName: teamName + } + }); + } catch (error) { + console.error("Error loading bazaar:", error); + element.innerHTML = ` +
+

Error Loading Bazaar

+

Failed to load bazaar. Please try again later.

+
`; + } + + resolve(); + }); + }; }; diff --git a/static/js/trading.js b/static/js/trading.js new file mode 100644 index 0000000..abfdf3a --- /dev/null +++ b/static/js/trading.js @@ -0,0 +1,454 @@ +window.initBazaar = function (opts) { + opts = opts || {}; + window._bazaarInitialized = true; + + let categories = [ + { value: "mechanical", label: "Mechanical" }, + { value: "electrical", label: "Electrical" }, + { value: "pneumatics", label: "Pneumatics" }, + { value: "tools", label: "Tools" }, + { value: "parts", label: "Parts" }, + { value: "other", label: "Other" } + ]; + + let listings = []; + let currentFilter = "all"; + let currentCategory = "all"; + let currentEvent = "all"; + let currentSearch = ""; + let selectedType = "offer"; + let savedTeam = opts.team || null; + + function applyTeamToForm() { + if (!savedTeam) { + return; + } + if (document.getElementById("form-team")) { + document.getElementById("form-team").value = savedTeam.team; + } + if (document.getElementById("form-team-name")) { + document.getElementById("form-team-name").value = + savedTeam.teamName || ""; + } + } + + if (savedTeam) { + applyTeamToForm(); + } + + function renderListings() { + let filtered = listings.filter(function (listing) { + if (currentFilter !== "all" && listing.type !== currentFilter) { + return false; + } + if ( + currentCategory !== "all" && + listing.category !== currentCategory + ) { + return false; + } + if (currentEvent !== "all" && listing.event !== currentEvent) { + return false; + } + if (currentSearch) { + let q = currentSearch.toLowerCase(); + let searchable = ( + listing.item + + " " + + (listing.teamName || "") + + " " + + listing.team + + " " + + (listing.description || "") + + " " + + listing.category + ).toLowerCase(); + if (searchable.indexOf(q) === -1) { + return false; + } + } + + return true; + }); + + if (filtered.length === 0) { + document.getElementById("trading-listings").style.display = "none"; + document.getElementById("trading-empty").style.display = "block"; + return; + } + + document.getElementById("trading-listings").style.display = "grid"; + document.getElementById("trading-empty").style.display = "none"; + + document.getElementById("trading-listings").innerHTML = filtered + .map(function (listing) { + let timeago = getTimeAgo(new Date(listing.timestamp).getTime()); + let owned = savedTeam && listing.team === savedTeam.team; + return ( + '
' + + '
' + + '' + + listing.type + + "" + + '' + + formatCategory(listing.category) + + "" + + "
" + + '

' + + escapeHtml(listing.item) + + "

" + + '

' + + "Team " + + listing.team + + (listing.teamName + ? " — " + escapeHtml(listing.teamName) + : "") + + "

" + + (listing.description + ? '

' + + escapeHtml(listing.description) + + "

" + : "") + + '" + + "
" + ); + }) + .join(""); + + updateStats(); + } + + function updateStats() { + let offers = listings.filter(function (l) { + return l.type === "offer"; + }).length; + let requests = listings.filter(function (l) { + return l.type === "request"; + }).length; + let teams = []; + listings.forEach(function (l) { + if (teams.indexOf(l.team) === -1) teams.push(l.team); + }); + + setStatText("stat-offers", offers); + setStatText("stat-requests", requests); + setStatText("stat-teams", teams.length); + setStatText("stat-total", listings.length); + } + + function setStatText(id, val) { + let el = document.getElementById(id); + if (el) el.textContent = val; + } + + function fetchListings() { + fetch("/api/v1/trading/listings") + .then(function (res) { + return res.json(); + }) + .then(function (data) { + if (data.success && data.body && data.body.listings) { + listings = data.body.listings; + renderListings(); + } + }) + .catch(function (err) { + console.warn("Could not load listings:", err); + }); + } + + function submitListing(formData) { + fetch("/api/v1/trading/listings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData) + }) + .then(function (res) { + return res.json(); + }) + .then(function (data) { + if (data.success && data.body && data.body.listing) { + listings.unshift(data.body.listing); + renderListings(); + showFormSuccess(formData); + } else { + alert( + "Error: " + (data.error || "Could not create listing") + ); + } + }) + .catch(function () { + alert("Network error. Please try again."); + }); + } + + window.deleteListing = function (id) { + if (!savedTeam) return; + if (!confirm("Delete this listing?")) return; + + fetch("/api/v1/trading/listings/" + id, { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ team: savedTeam.team }) + }) + .then(function (res) { + return res.json(); + }) + .then(function (data) { + if (data.success) { + listings = listings.filter(function (l) { + return l._id !== id; + }); + renderListings(); + } else { + alert("Error: " + (data.error || "Could not delete")); + } + }) + .catch(function () { + alert("Network error. Please try again."); + }); + }; + + function showFormSuccess(formData) { + document.getElementById("trading-form").style.display = "none"; + let success = document.createElement("div"); + success.className = "trading-success"; + success.style.display = "block"; + success.innerHTML = + "

Listing Posted!

" + + "

Your " + + formData.type + + ' for "' + + escapeHtml(formData.item) + + '" is now live.

' + + "" + + '

Post another'; + document.getElementById("trading-form").parentNode.appendChild(success); + + success + .querySelector(".trading-post-another") + .addEventListener("click", function (ev) { + ev.preventDefault(); + success.remove(); + document.getElementById("trading-form").style.display = "block"; + document.getElementById("trading-form").reset(); + applyTeamToForm(); + }); + } + + function formatCategory(cat) { + for (let i = 0; i < categories.length; i++) { + if (categories[i].value === cat) return categories[i].label; + } + return cat.replace(/-/g, " ").replace(/\b\w/g, function (c) { + return c.toUpperCase(); + }); + } + + function escapeHtml(str) { + if (!str) return ""; + let div = document.createElement("div"); + div.appendChild(document.createTextNode(str)); + return div.innerHTML; + } + + function getTimeAgo(timestamp) { + let diff = Date.now() - timestamp; + let mins = Math.floor(diff / 60000); + if (mins < 60) return mins + "m ago"; + let hours = Math.floor(mins / 60); + if (hours < 24) return hours + "h ago"; + let days = Math.floor(hours / 24); + return days + "d ago"; + } + + window.showContact = function (btn, contact) { + btn.innerHTML = escapeHtml(contact); + btn.style.fontSize = "0.7rem"; + btn.style.cursor = "default"; + btn.onclick = null; + }; + + window.goToForm = function (typeId) { + let btn = document.getElementById(typeId); + if (btn) btn.click(); + let post = document.getElementById("post"); + if (post) post.scrollIntoView({ behavior: "smooth" }); + }; + + document.querySelectorAll(".trading-tab").forEach(function (tab) { + tab.addEventListener("click", function () { + document.querySelectorAll(".trading-tab").forEach(function (t) { + t.classList.remove("active"); + }); + tab.classList.add("active"); + currentFilter = tab.getAttribute("data-filter"); + renderListings(); + }); + }); + + if (document.getElementById("trading-search")) { + document + .getElementById("trading-search") + .addEventListener("input", function () { + currentSearch = document + .getElementById("trading-search") + .value.trim(); + renderListings(); + }); + } + + if (document.getElementById("trading-category")) { + document + .getElementById("trading-category") + .addEventListener("change", function () { + currentCategory = + document.getElementById("trading-category").value; + renderListings(); + }); + } + + if (document.getElementById("trading-event-filter")) { + document + .getElementById("trading-event-filter") + .addEventListener("change", function () { + currentEvent = document.getElementById( + "trading-event-filter" + ).value; + renderListings(); + }); + } + + document.querySelectorAll(".trading-type-btn").forEach(function (btn) { + btn.addEventListener("click", function () { + document + .querySelectorAll(".trading-type-btn") + .forEach(function (b) { + b.classList.remove("active"); + }); + btn.classList.add("active"); + selectedType = btn.getAttribute("data-type"); + }); + }); + + if (document.getElementById("trading-form")) { + document + .getElementById("trading-form") + .addEventListener("submit", function (e) { + e.preventDefault(); + + if (!savedTeam) { + return; + } + + if ( + !document.getElementById("form-item").value.trim() || + !document.getElementById("form-contact").value.trim() + ) { + return; + } + + submitListing({ + type: selectedType, + team: savedTeam.team, + teamName: savedTeam.teamName, + item: document.getElementById("form-item").value.trim(), + category: document.getElementById("form-category").value, + quantity: + parseInt( + document.getElementById("form-quantity").value + ) || 1, + description: document + .getElementById("form-description") + .value.trim(), + contact: document + .getElementById("form-contact") + .value.trim(), + event: document.getElementById("form-event").value + }); + }); + } + + function fetchEvents() { + let year = new Date().getFullYear(); + fetch(`/api/v1/scouting/events/${encodeURIComponent(year)}/team`) + .then(function (res) { + return res.json(); + }) + .then(function (data) { + if (data.success && data.body && data.body.events) { + const filteredEvents = data.body.events.filter(function ( + evt + ) { + return evt.name.includes("*"); + }); + populateEventDropdowns(filteredEvents); + } + }) + .catch(function (err) { + console.warn("Could not load events from TBA:", err); + }); + } + + function populateEventDropdowns(events) { + if (document.getElementById("trading-event-filter")) { + events.forEach(function (evt) { + let option = document.createElement("option"); + option.value = evt.key; + option.textContent = evt.name; + document + .getElementById("trading-event-filter") + .appendChild(option); + }); + } + if (document.getElementById("form-event")) { + events.forEach(function (evt) { + let option = document.createElement("option"); + option.value = evt.key; + option.textContent = evt.name; + document.getElementById("form-event").appendChild(option); + }); + } + } + + function populateCategories() { + let selects = [ + document.getElementById("trading-category"), + document.getElementById("form-category") + ]; + selects.forEach(function (sel) { + if (!sel) return; + categories.forEach(function (cat) { + let option = document.createElement("option"); + option.value = cat.value; + option.textContent = cat.label; + sel.appendChild(option); + }); + }); + } + + populateCategories(); + applyTeamToForm(); + fetchListings(); + fetchEvents(); +}; diff --git a/views/scouting/index.hbs b/views/scouting/index.hbs index e2a1d73..ca60d5d 100644 --- a/views/scouting/index.hbs +++ b/views/scouting/index.hbs @@ -58,7 +58,6 @@ const inv = await fetch("/api/v1/scouting/shop/inventory"); const invdata = await inv.json(); const inventory = invdata.body?.inventory || []; - console.log(inventory); const ownsDarkMode = inventory.some(entry => (entry.itemId?._id)); if (ownsDarkMode) { document.querySelector(".dark-mode-toggle-scouting").style.display = "flex"; @@ -94,6 +93,9 @@ } else if(page == "shop") { document.querySelector(".navbar a[data-page='shop']").classList.add("active"); await sdk.showShopPage(); + } else if(page == "bazaar") { + document.querySelector(".navbar a[data-page='bazaar']").classList.add("active"); + await sdk.showBazaarPage(); } else { document.querySelector(".navbar a[data-page='home']").classList.add("active"); await sdk.showHomePage(); diff --git a/views/scouting/partials/scoutingHeader.hbs b/views/scouting/partials/scoutingHeader.hbs index 6410773..40b4bf0 100644 --- a/views/scouting/partials/scoutingHeader.hbs +++ b/views/scouting/partials/scoutingHeader.hbs @@ -12,6 +12,7 @@ Predict Leaderboard Shop + Log Out
+ + + + +
+

Browse Listings

+ +
+
+ + + +
+
+ +
+ + +
+ +
+ + +
+ +
+

Post a Listing

+

Whether you have spare parts to share or need something to compete, fill out the form to connect with + other teams.

+ +
+
+ +
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + + +
+ +
+ + +
+ +
+ + + +
+ +
+ + +
+ +
+ + + +
+ +
+ + +
+ + +
+
+ + + + + + + + + \ No newline at end of file