mirror of
https://github.com/System-End/scraps.git
synced 2026-04-19 19:45:14 +00:00
fix bugs and compensation and add in admin tools
This commit is contained in:
parent
2b3d38a5f2
commit
09b10ac900
18 changed files with 682 additions and 176 deletions
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "app",
|
||||
|
|
@ -161,11 +162,11 @@
|
|||
|
||||
"openid-client": ["openid-client@6.8.1", "", { "dependencies": { "jose": "^6.1.0", "oauth4webapi": "^3.8.2" } }, "sha512-VoYT6enBo6Vj2j3Q5Ec0AezS+9YGzQo1f5Xc42lreMGlfP4ljiXPKVDvCADh+XHCV/bqPu/wWSiCVXbJKvrODw=="],
|
||||
|
||||
"pg": ["pg@8.17.2", "", { "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw=="],
|
||||
"pg": ["pg@8.18.0", "", { "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ=="],
|
||||
|
||||
"pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="],
|
||||
|
||||
"pg-connection-string": ["pg-connection-string@2.10.1", "", {}, "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw=="],
|
||||
"pg-connection-string": ["pg-connection-string@2.11.0", "", {}, "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ=="],
|
||||
|
||||
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||
|
||||
|
|
|
|||
117
backend/dist/index.js
vendored
117
backend/dist/index.js
vendored
|
|
@ -1819,8 +1819,8 @@ function mergeArrays(chunks) {
|
|||
var import_debug, debug, syncBufferSize, ddSignatureArray, eocdSignatureBytes;
|
||||
var init_ZipHandler = __esm(() => {
|
||||
init_lib3();
|
||||
import_debug = __toESM(require_src(), 1);
|
||||
init_ZipToken();
|
||||
import_debug = __toESM(require_src(), 1);
|
||||
debug = import_debug.default("tokenizer:inflate");
|
||||
syncBufferSize = 256 * 1024;
|
||||
ddSignatureArray = signatureToArray(Signature.DataDescriptor);
|
||||
|
|
@ -5848,7 +5848,6 @@ var require_type_overrides = __commonJS((exports, module) => {
|
|||
|
||||
// node_modules/pg-connection-string/index.js
|
||||
var require_pg_connection_string = __commonJS((exports, module) => {
|
||||
var { emitWarning } = __require("process");
|
||||
function parse2(str, options = {}) {
|
||||
if (str.charAt(0) === "/") {
|
||||
const config3 = str.split(" ");
|
||||
|
|
@ -6007,9 +6006,9 @@ var require_pg_connection_string = __commonJS((exports, module) => {
|
|||
return toClientConfig(parse2(str));
|
||||
}
|
||||
function deprecatedSslModeWarning(sslmode) {
|
||||
if (!deprecatedSslModeWarning.warned) {
|
||||
if (!deprecatedSslModeWarning.warned && typeof process !== "undefined" && process.emitWarning) {
|
||||
deprecatedSslModeWarning.warned = true;
|
||||
emitWarning(`SECURITY WARNING: The SSL modes 'prefer', 'require', and 'verify-ca' are treated as aliases for 'verify-full'.
|
||||
process.emitWarning(`SECURITY WARNING: The SSL modes 'prefer', 'require', and 'verify-ca' are treated as aliases for 'verify-full'.
|
||||
In the next major version (pg-connection-string v3.0.0 and pg v9.0.0), these modes will adopt standard libpq semantics, which have weaker security guarantees.
|
||||
|
||||
To prepare for this change:
|
||||
|
|
@ -7917,7 +7916,7 @@ var require_client = __commonJS((exports, module) => {
|
|||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
resolve(this);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -8073,16 +8072,40 @@ var require_client = __commonJS((exports, module) => {
|
|||
activeQuery.handleError(msg, this.connection);
|
||||
}
|
||||
_handleRowDescription(msg) {
|
||||
this._getActiveQuery().handleRowDescription(msg);
|
||||
const activeQuery = this._getActiveQuery();
|
||||
if (activeQuery == null) {
|
||||
const error = new Error("Received unexpected rowDescription message from backend.");
|
||||
this._handleErrorEvent(error);
|
||||
return;
|
||||
}
|
||||
activeQuery.handleRowDescription(msg);
|
||||
}
|
||||
_handleDataRow(msg) {
|
||||
this._getActiveQuery().handleDataRow(msg);
|
||||
const activeQuery = this._getActiveQuery();
|
||||
if (activeQuery == null) {
|
||||
const error = new Error("Received unexpected dataRow message from backend.");
|
||||
this._handleErrorEvent(error);
|
||||
return;
|
||||
}
|
||||
activeQuery.handleDataRow(msg);
|
||||
}
|
||||
_handlePortalSuspended(msg) {
|
||||
this._getActiveQuery().handlePortalSuspended(this.connection);
|
||||
const activeQuery = this._getActiveQuery();
|
||||
if (activeQuery == null) {
|
||||
const error = new Error("Received unexpected portalSuspended message from backend.");
|
||||
this._handleErrorEvent(error);
|
||||
return;
|
||||
}
|
||||
activeQuery.handlePortalSuspended(this.connection);
|
||||
}
|
||||
_handleEmptyQuery(msg) {
|
||||
this._getActiveQuery().handleEmptyQuery(this.connection);
|
||||
const activeQuery = this._getActiveQuery();
|
||||
if (activeQuery == null) {
|
||||
const error = new Error("Received unexpected emptyQuery message from backend.");
|
||||
this._handleErrorEvent(error);
|
||||
return;
|
||||
}
|
||||
activeQuery.handleEmptyQuery(this.connection);
|
||||
}
|
||||
_handleCommandComplete(msg) {
|
||||
const activeQuery = this._getActiveQuery();
|
||||
|
|
@ -8105,10 +8128,22 @@ var require_client = __commonJS((exports, module) => {
|
|||
}
|
||||
}
|
||||
_handleCopyInResponse(msg) {
|
||||
this._getActiveQuery().handleCopyInResponse(this.connection);
|
||||
const activeQuery = this._getActiveQuery();
|
||||
if (activeQuery == null) {
|
||||
const error = new Error("Received unexpected copyInResponse message from backend.");
|
||||
this._handleErrorEvent(error);
|
||||
return;
|
||||
}
|
||||
activeQuery.handleCopyInResponse(this.connection);
|
||||
}
|
||||
_handleCopyData(msg) {
|
||||
this._getActiveQuery().handleCopyData(msg, this.connection);
|
||||
const activeQuery = this._getActiveQuery();
|
||||
if (activeQuery == null) {
|
||||
const error = new Error("Received unexpected copyData message from backend.");
|
||||
this._handleErrorEvent(error);
|
||||
return;
|
||||
}
|
||||
activeQuery.handleCopyData(msg, this.connection);
|
||||
}
|
||||
_handleNotification(msg) {
|
||||
this.emit("notification", msg);
|
||||
|
|
@ -8902,7 +8937,7 @@ var require_client2 = __commonJS((exports, module) => {
|
|||
});
|
||||
self.emit("connect");
|
||||
self._pulseQueryQueue(true);
|
||||
cb();
|
||||
cb(null, this);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
@ -8916,7 +8951,7 @@ var require_client2 = __commonJS((exports, module) => {
|
|||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
resolve(this);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -28575,16 +28610,24 @@ projects.get("/:id", async ({ params, headers }) => {
|
|||
reviewer: reviewers.find((rv) => rv.id === r.reviewerId) || null
|
||||
});
|
||||
}
|
||||
const submissions = await db.select({
|
||||
const activityEntries = await db.select({
|
||||
id: activityTable.id,
|
||||
action: activityTable.action,
|
||||
createdAt: activityTable.createdAt
|
||||
}).from(activityTable).where(and(eq(activityTable.projectId, parseInt(params.id)), eq(activityTable.action, "project_submitted")));
|
||||
for (const s of submissions) {
|
||||
activity.push({
|
||||
type: "submitted",
|
||||
createdAt: s.createdAt
|
||||
});
|
||||
}).from(activityTable).where(and(eq(activityTable.projectId, parseInt(params.id)), or(eq(activityTable.action, "project_submitted"), sql`${activityTable.action} LIKE 'earned % scraps'`)));
|
||||
for (const entry of activityEntries) {
|
||||
if (entry.action === "project_submitted") {
|
||||
activity.push({
|
||||
type: "submitted",
|
||||
createdAt: entry.createdAt
|
||||
});
|
||||
} else if (entry.action.startsWith("earned ") && entry.action.endsWith(" scraps")) {
|
||||
activity.push({
|
||||
type: "scraps_earned",
|
||||
action: entry.action,
|
||||
createdAt: entry.createdAt
|
||||
});
|
||||
}
|
||||
}
|
||||
activity.push({
|
||||
type: "created",
|
||||
|
|
@ -28608,6 +28651,7 @@ projects.get("/:id", async ({ params, headers }) => {
|
|||
hours: project[0].hoursOverride ?? project[0].hours,
|
||||
hoursOverride: isOwner ? project[0].hoursOverride : undefined,
|
||||
status: project[0].status,
|
||||
scrapsAwarded: project[0].scrapsAwarded,
|
||||
createdAt: project[0].createdAt,
|
||||
updatedAt: project[0].updatedAt
|
||||
},
|
||||
|
|
@ -28903,7 +28947,7 @@ var shopPenaltiesTable = pgTable("shop_penalties", {
|
|||
|
||||
// src/lib/scraps.ts
|
||||
var PHI = (1 + Math.sqrt(5)) / 2;
|
||||
var MULTIPLIER = 120;
|
||||
var MULTIPLIER = 10;
|
||||
function calculateScrapsFromHours(hours) {
|
||||
return Math.floor(hours * PHI * MULTIPLIER);
|
||||
}
|
||||
|
|
@ -29159,6 +29203,7 @@ shop.get("/items", async ({ headers }) => {
|
|||
baseProbability: shopItemsTable.baseProbability,
|
||||
baseUpgradeCost: shopItemsTable.baseUpgradeCost,
|
||||
costMultiplier: shopItemsTable.costMultiplier,
|
||||
boostAmount: shopItemsTable.boostAmount,
|
||||
createdAt: shopItemsTable.createdAt,
|
||||
updatedAt: shopItemsTable.updatedAt,
|
||||
heartCount: sql`(SELECT COUNT(*) FROM shop_hearts WHERE shop_item_id = ${shopItemsTable.id})`.as("heart_count")
|
||||
|
|
@ -29212,6 +29257,7 @@ shop.get("/items/:id", async ({ params, headers }) => {
|
|||
baseProbability: shopItemsTable.baseProbability,
|
||||
baseUpgradeCost: shopItemsTable.baseUpgradeCost,
|
||||
costMultiplier: shopItemsTable.costMultiplier,
|
||||
boostAmount: shopItemsTable.boostAmount,
|
||||
createdAt: shopItemsTable.createdAt,
|
||||
updatedAt: shopItemsTable.updatedAt,
|
||||
heartCount: sql`(SELECT COUNT(*) FROM shop_hearts WHERE shop_item_id = ${shopItemsTable.id})`.as("heart_count")
|
||||
|
|
@ -29272,7 +29318,9 @@ shop.post("/items/:id/heart", async ({ params, headers }) => {
|
|||
SELECT EXISTS(SELECT 1 FROM ins) AS hearted
|
||||
`);
|
||||
const hearted = result.rows[0]?.hearted ?? false;
|
||||
return { hearted };
|
||||
const countResult = await db.select({ count: sql`COUNT(*)` }).from(shopHeartsTable).where(eq(shopHeartsTable.shopItemId, itemId));
|
||||
const heartCount = Number(countResult[0]?.count) || 0;
|
||||
return { hearted, heartCount };
|
||||
});
|
||||
shop.get("/categories", async () => {
|
||||
const result = await db.selectDistinct({ category: shopItemsTable.category }).from(shopItemsTable);
|
||||
|
|
@ -29366,6 +29414,9 @@ shop.get("/orders", async ({ headers }) => {
|
|||
pricePerItem: shopOrdersTable.pricePerItem,
|
||||
totalPrice: shopOrdersTable.totalPrice,
|
||||
status: shopOrdersTable.status,
|
||||
orderType: shopOrdersTable.orderType,
|
||||
shippingAddress: shopOrdersTable.shippingAddress,
|
||||
isFulfilled: shopOrdersTable.isFulfilled,
|
||||
createdAt: shopOrdersTable.createdAt,
|
||||
itemId: shopItemsTable.id,
|
||||
itemName: shopItemsTable.name,
|
||||
|
|
@ -29451,12 +29502,23 @@ shop.post("/items/:id/try-luck", async ({ params, headers }) => {
|
|||
}
|
||||
return { won: true, orderId: newOrder[0].id, effectiveProbability, rolled };
|
||||
}
|
||||
return { won: false, effectiveProbability, rolled };
|
||||
const consolationOrder = await tx.insert(shopOrdersTable).values({
|
||||
userId: user2.id,
|
||||
shopItemId: itemId,
|
||||
quantity: 1,
|
||||
pricePerItem: item.price,
|
||||
totalPrice: item.price,
|
||||
shippingAddress: null,
|
||||
status: "pending",
|
||||
orderType: "consolation",
|
||||
notes: `Consolation scrap paper - rolled ${rolled}, needed ${effectiveProbability} or less`
|
||||
}).returning();
|
||||
return { won: false, effectiveProbability, rolled, consolationOrderId: consolationOrder[0].id };
|
||||
});
|
||||
if (result.won) {
|
||||
return { success: true, won: true, orderId: result.orderId, effectiveProbability: result.effectiveProbability, rolled: result.rolled, refineryReset: true, probabilityHalved: true };
|
||||
}
|
||||
return { success: true, won: false, effectiveProbability: result.effectiveProbability, rolled: result.rolled };
|
||||
return { success: true, won: false, consolationOrderId: result.consolationOrderId, effectiveProbability: result.effectiveProbability, rolled: result.rolled };
|
||||
} catch (e) {
|
||||
const err = e;
|
||||
if (err.type === "insufficient_funds") {
|
||||
|
|
@ -29502,15 +29564,16 @@ shop.post("/items/:id/upgrade-probability", async ({ params, headers }) => {
|
|||
const { balance } = await getUserScrapsBalance(user2.id, tx);
|
||||
throw { type: "insufficient_funds", balance, cost };
|
||||
}
|
||||
const newBoost = currentBoost + 1;
|
||||
const boostAmount = item.boostAmount;
|
||||
const newBoost = currentBoost + boostAmount;
|
||||
await tx.insert(refineryOrdersTable).values({
|
||||
userId: user2.id,
|
||||
shopItemId: itemId,
|
||||
cost,
|
||||
boostAmount: 1
|
||||
boostAmount
|
||||
});
|
||||
const nextCost = newBoost >= maxBoost ? null : Math.floor(item.baseUpgradeCost * Math.pow(item.costMultiplier / 100, newBoost));
|
||||
return { boostPercent: newBoost, nextCost, effectiveProbability: Math.min(adjustedBaseProbability + newBoost, 100) };
|
||||
return { boostPercent: newBoost, boostAmount, nextCost, effectiveProbability: Math.min(adjustedBaseProbability + newBoost, 100) };
|
||||
});
|
||||
return result;
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
"drizzle-orm": "^0.45.1",
|
||||
"elysia": "latest",
|
||||
"openid-client": "^6.8.1",
|
||||
"pg": "^8.17.2"
|
||||
"pg": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pg": "^8.16.0",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { shopOrdersTable, refineryOrdersTable } from '../schemas/shop'
|
|||
import { userBonusesTable } from '../schemas/users'
|
||||
|
||||
export const PHI = (1 + Math.sqrt(5)) / 2
|
||||
export const MULTIPLIER = 120
|
||||
export const MULTIPLIER = 10
|
||||
|
||||
export function calculateScrapsFromHours(hours: number): number {
|
||||
return Math.floor(hours * PHI * MULTIPLIER)
|
||||
|
|
|
|||
|
|
@ -151,8 +151,8 @@ projects.get('/:id', async ({ params, headers }) => {
|
|||
})
|
||||
}
|
||||
|
||||
// Fetch submission events from activity table
|
||||
const submissions = await db
|
||||
// Fetch submission and scraps earned events from activity table
|
||||
const activityEntries = await db
|
||||
.select({
|
||||
id: activityTable.id,
|
||||
action: activityTable.action,
|
||||
|
|
@ -161,14 +161,25 @@ projects.get('/:id', async ({ params, headers }) => {
|
|||
.from(activityTable)
|
||||
.where(and(
|
||||
eq(activityTable.projectId, parseInt(params.id)),
|
||||
eq(activityTable.action, 'project_submitted')
|
||||
or(
|
||||
eq(activityTable.action, 'project_submitted'),
|
||||
sql`${activityTable.action} LIKE 'earned % scraps'`
|
||||
)
|
||||
))
|
||||
|
||||
for (const s of submissions) {
|
||||
activity.push({
|
||||
type: 'submitted',
|
||||
createdAt: s.createdAt
|
||||
})
|
||||
for (const entry of activityEntries) {
|
||||
if (entry.action === 'project_submitted') {
|
||||
activity.push({
|
||||
type: 'submitted',
|
||||
createdAt: entry.createdAt
|
||||
})
|
||||
} else if (entry.action.startsWith('earned ') && entry.action.endsWith(' scraps')) {
|
||||
activity.push({
|
||||
type: 'scraps_earned',
|
||||
action: entry.action,
|
||||
createdAt: entry.createdAt
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add "project created" entry
|
||||
|
|
@ -197,6 +208,7 @@ projects.get('/:id', async ({ params, headers }) => {
|
|||
hours: project[0].hoursOverride ?? project[0].hours,
|
||||
hoursOverride: isOwner ? project[0].hoursOverride : undefined,
|
||||
status: project[0].status,
|
||||
scrapsAwarded: project[0].scrapsAwarded,
|
||||
createdAt: project[0].createdAt,
|
||||
updatedAt: project[0].updatedAt
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ shop.get('/items', async ({ headers }) => {
|
|||
baseProbability: shopItemsTable.baseProbability,
|
||||
baseUpgradeCost: shopItemsTable.baseUpgradeCost,
|
||||
costMultiplier: shopItemsTable.costMultiplier,
|
||||
boostAmount: shopItemsTable.boostAmount,
|
||||
createdAt: shopItemsTable.createdAt,
|
||||
updatedAt: shopItemsTable.updatedAt,
|
||||
heartCount: sql<number>`(SELECT COUNT(*) FROM shop_hearts WHERE shop_item_id = ${shopItemsTable.id})`.as('heart_count')
|
||||
|
|
@ -96,6 +97,7 @@ shop.get('/items/:id', async ({ params, headers }) => {
|
|||
baseProbability: shopItemsTable.baseProbability,
|
||||
baseUpgradeCost: shopItemsTable.baseUpgradeCost,
|
||||
costMultiplier: shopItemsTable.costMultiplier,
|
||||
boostAmount: shopItemsTable.boostAmount,
|
||||
createdAt: shopItemsTable.createdAt,
|
||||
updatedAt: shopItemsTable.updatedAt,
|
||||
heartCount: sql<number>`(SELECT COUNT(*) FROM shop_hearts WHERE shop_item_id = ${shopItemsTable.id})`.as('heart_count')
|
||||
|
|
@ -200,7 +202,16 @@ shop.post('/items/:id/heart', async ({ params, headers }) => {
|
|||
`)
|
||||
|
||||
const hearted = (result.rows[0] as { hearted: boolean })?.hearted ?? false
|
||||
return { hearted }
|
||||
|
||||
// Get the updated heart count
|
||||
const countResult = await db
|
||||
.select({ count: sql<number>`COUNT(*)` })
|
||||
.from(shopHeartsTable)
|
||||
.where(eq(shopHeartsTable.shopItemId, itemId))
|
||||
|
||||
const heartCount = Number(countResult[0]?.count) || 0
|
||||
|
||||
return { hearted, heartCount }
|
||||
})
|
||||
|
||||
shop.get('/categories', async () => {
|
||||
|
|
@ -336,6 +347,9 @@ shop.get('/orders', async ({ headers }) => {
|
|||
pricePerItem: shopOrdersTable.pricePerItem,
|
||||
totalPrice: shopOrdersTable.totalPrice,
|
||||
status: shopOrdersTable.status,
|
||||
orderType: shopOrdersTable.orderType,
|
||||
shippingAddress: shopOrdersTable.shippingAddress,
|
||||
isFulfilled: shopOrdersTable.isFulfilled,
|
||||
createdAt: shopOrdersTable.createdAt,
|
||||
itemId: shopItemsTable.id,
|
||||
itemName: shopItemsTable.name,
|
||||
|
|
@ -500,13 +514,29 @@ shop.post('/items/:id/try-luck', async ({ params, headers }) => {
|
|||
return { won: true, orderId: newOrder[0].id, effectiveProbability, rolled }
|
||||
}
|
||||
|
||||
return { won: false, effectiveProbability, rolled }
|
||||
// Create consolation order for scrap paper when user loses
|
||||
const consolationOrder = await tx
|
||||
.insert(shopOrdersTable)
|
||||
.values({
|
||||
userId: user.id,
|
||||
shopItemId: itemId,
|
||||
quantity: 1,
|
||||
pricePerItem: item.price,
|
||||
totalPrice: item.price,
|
||||
shippingAddress: null,
|
||||
status: 'pending',
|
||||
orderType: 'consolation',
|
||||
notes: `Consolation scrap paper - rolled ${rolled}, needed ${effectiveProbability} or less`
|
||||
})
|
||||
.returning()
|
||||
|
||||
return { won: false, effectiveProbability, rolled, consolationOrderId: consolationOrder[0].id }
|
||||
})
|
||||
|
||||
if (result.won) {
|
||||
return { success: true, won: true, orderId: result.orderId, effectiveProbability: result.effectiveProbability, rolled: result.rolled, refineryReset: true, probabilityHalved: true }
|
||||
}
|
||||
return { success: true, won: false, effectiveProbability: result.effectiveProbability, rolled: result.rolled }
|
||||
return { success: true, won: false, consolationOrderId: result.consolationOrderId, effectiveProbability: result.effectiveProbability, rolled: result.rolled }
|
||||
} catch (e) {
|
||||
const err = e as { type?: string; balance?: number }
|
||||
if (err.type === 'insufficient_funds') {
|
||||
|
|
@ -584,21 +614,22 @@ shop.post('/items/:id/upgrade-probability', async ({ params, headers }) => {
|
|||
throw { type: 'insufficient_funds', balance, cost }
|
||||
}
|
||||
|
||||
const newBoost = currentBoost + 1
|
||||
const boostAmount = item.boostAmount
|
||||
const newBoost = currentBoost + boostAmount
|
||||
|
||||
// Record the refinery order
|
||||
await tx.insert(refineryOrdersTable).values({
|
||||
userId: user.id,
|
||||
shopItemId: itemId,
|
||||
cost,
|
||||
boostAmount: 1
|
||||
boostAmount
|
||||
})
|
||||
|
||||
const nextCost = newBoost >= maxBoost
|
||||
? null
|
||||
: Math.floor(item.baseUpgradeCost * Math.pow(item.costMultiplier / 100, newBoost))
|
||||
|
||||
return { boostPercent: newBoost, nextCost, effectiveProbability: Math.min(adjustedBaseProbability + newBoost, 100) }
|
||||
return { boostPercent: newBoost, boostAmount, nextCost, effectiveProbability: Math.min(adjustedBaseProbability + newBoost, 100) }
|
||||
})
|
||||
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "scraps",
|
||||
|
|
@ -104,13 +105,13 @@
|
|||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
"@eslint/compat": ["@eslint/compat@2.0.1", "", { "dependencies": { "@eslint/core": "^1.0.1" }, "peerDependencies": { "eslint": "^8.40 || 9" }, "optionalPeers": ["eslint"] }, "sha512-yl/JsgplclzuvGFNqwNYV4XNPhP3l62ZOP9w/47atNAdmDtIFCx6X7CSk/SlWUuBGkT4Et/5+UD+WyvX2iiIWA=="],
|
||||
"@eslint/compat": ["@eslint/compat@2.0.2", "", { "dependencies": { "@eslint/core": "^1.1.0" }, "peerDependencies": { "eslint": "^8.40 || 9 || 10" }, "optionalPeers": ["eslint"] }, "sha512-pR1DoD0h3HfF675QZx0xsyrsU8q70Z/plx7880NOhS02NuWLgBCOMDL787nUeQ7EWLkxv3bPQJaarjcPQb2Dwg=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@1.0.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-r18fEAj9uCk+VjzGt2thsbOmychS+4kxI14spVNibUO2vqKX7obOG+ymZljAwuPZl+S3clPGwCwTDtrdqTiY6Q=="],
|
||||
"@eslint/core": ["@eslint/core@1.1.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="],
|
||||
|
||||
|
|
@ -204,7 +205,7 @@
|
|||
|
||||
"@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="],
|
||||
|
||||
"@sveltejs/kit": ["@sveltejs/kit@2.50.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-XRHD2i3zC4ukhz2iCQzO4mbsts081PAZnnMAQ7LNpWeYgeBmwMsalf0FGSwhFXBbtr2XViPKnFJBDCckWqrsLw=="],
|
||||
"@sveltejs/kit": ["@sveltejs/kit@2.50.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-875hTUkEbz+MyJIxWbQjfMaekqdmEKUUfR7JyKcpfMRZqcGyrO9Gd+iS1D/Dx8LpE5FEtutWGOtlAh4ReSAiOA=="],
|
||||
|
||||
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
|
||||
|
||||
|
|
@ -248,29 +249,29 @@
|
|||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="],
|
||||
"@types/node": ["@types/node@22.19.8", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ebO/Yl+EAvVe8DnMfi+iaAyIqYdK0q/q0y0rw82INWEKJOBe6b/P3YWE8NW7oOlF/nXFNrHwhARrN/hdgDkraA=="],
|
||||
|
||||
"@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/type-utils": "8.53.1", "@typescript-eslint/utils": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag=="],
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.54.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/type-utils": "8.54.0", "@typescript-eslint/utils": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.54.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg=="],
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.54.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.53.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.53.1", "@typescript-eslint/types": "^8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog=="],
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.54.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.54.0", "@typescript-eslint/types": "^8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1" } }, "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ=="],
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0" } }, "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg=="],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.53.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA=="],
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.54.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/utils": "8.53.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w=="],
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.53.1", "", {}, "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A=="],
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.53.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.53.1", "@typescript-eslint/tsconfig-utils": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg=="],
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.54.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.54.0", "@typescript-eslint/tsconfig-utils": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg=="],
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.54.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg=="],
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
|
|
@ -386,7 +387,7 @@
|
|||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"globals": ["globals@17.1.0", "", {}, "sha512-8HoIcWI5fCvG5NADj4bDav+er9B9JMj2vyL2pI8D0eismKyUvPLTSs+Ln3wqhwcp306i73iyVnEKx3F6T47TGw=="],
|
||||
"globals": ["globals@17.3.0", "", {}, "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
|
|
@ -552,7 +553,7 @@
|
|||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||
"set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
|
|
@ -572,9 +573,9 @@
|
|||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"svelte": ["svelte@5.48.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-VPWD+UyoSFZ7Nxix5K/F8yWiKWOiROkLlWYXOZReE0TUycw+58YWB3D6lAKT+57xmN99wRX4H3oZmw0NPy7y3Q=="],
|
||||
"svelte": ["svelte@5.49.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-jj95WnbKbXsXXngYj28a4zx8jeZx50CN/J4r0CEeax2pbfdsETv/J1K8V9Hbu3DCXnpHz5qAikICuxEooi7eNQ=="],
|
||||
|
||||
"svelte-check": ["svelte-check@4.3.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q=="],
|
||||
"svelte-check": ["svelte-check@4.3.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-uBkz96ElE3G4pt9E1Tw0xvBfIUQkeH794kDQZdAUk795UVMr+NJZpuFSS62vcmO/DuSalK83LyOwhgWq8YGU1Q=="],
|
||||
|
||||
"svelte-eslint-parser": ["svelte-eslint-parser@1.4.1", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA=="],
|
||||
|
||||
|
|
@ -594,7 +595,7 @@
|
|||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.53.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.1", "@typescript-eslint/parser": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/utils": "8.53.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg=="],
|
||||
"typescript-eslint": ["typescript-eslint@8.54.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.54.0", "@typescript-eslint/parser": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
|
|
@ -642,10 +643,14 @@
|
|||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@types/pg/@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"better-call/set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||
|
||||
"drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"eslint/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
|
||||
|
|
|
|||
|
|
@ -5,27 +5,27 @@
|
|||
"node": "22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^2.0.1",
|
||||
"@eslint/compat": "^2.0.2",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.50.1",
|
||||
"@sveltejs/kit": "^2.50.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/node": "^22",
|
||||
"@types/node": "^22.19.8",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.14.0",
|
||||
"globals": "^17.1.0",
|
||||
"globals": "^17.3.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.4.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"svelte": "^5.48.2",
|
||||
"svelte-check": "^4.3.5",
|
||||
"svelte": "^5.49.1",
|
||||
"svelte-check": "^4.3.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.53.1",
|
||||
"typescript-eslint": "^8.54.0",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"private": true,
|
||||
|
|
|
|||
|
|
@ -17,16 +17,20 @@
|
|||
primary: boolean
|
||||
}
|
||||
|
||||
import type { Snippet } from 'svelte'
|
||||
|
||||
let {
|
||||
orderId,
|
||||
itemName,
|
||||
onClose,
|
||||
onComplete
|
||||
onComplete,
|
||||
header
|
||||
}: {
|
||||
orderId: number
|
||||
itemName: string
|
||||
onClose: () => void
|
||||
onComplete: () => void
|
||||
header?: Snippet
|
||||
} = $props()
|
||||
|
||||
let addresses = $state<Address[]>([])
|
||||
|
|
@ -127,13 +131,17 @@
|
|||
<h2 class="text-2xl font-bold">shipping address</h2>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 p-4 border-2 border-black rounded-lg bg-gray-50">
|
||||
<p class="text-lg font-bold">🎉 congratulations!</p>
|
||||
<p class="text-gray-600 mt-1">
|
||||
you won <span class="font-bold">{itemName}</span>! select your shipping address to receive
|
||||
it.
|
||||
</p>
|
||||
</div>
|
||||
{#if header}
|
||||
{@render header()}
|
||||
{:else}
|
||||
<div class="mb-6 p-4 border-2 border-black rounded-lg bg-gray-50">
|
||||
<p class="text-lg font-bold">🎉 congratulations!</p>
|
||||
<p class="text-gray-600 mt-1">
|
||||
you won <span class="font-bold">{itemName}</span>! select your shipping address to receive
|
||||
it.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div class="mb-4 p-3 bg-red-100 border-2 border-red-500 rounded-lg text-red-700 text-sm">
|
||||
|
|
@ -213,7 +221,7 @@
|
|||
class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-black transition-colors"
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
manage addresses on hack club
|
||||
manage addresses on hack club auth
|
||||
</a>
|
||||
{:else}
|
||||
<div class="text-center py-6">
|
||||
|
|
|
|||
|
|
@ -29,11 +29,13 @@
|
|||
let {
|
||||
item,
|
||||
onClose,
|
||||
onTryLuck
|
||||
onTryLuck,
|
||||
onConsolation
|
||||
}: {
|
||||
item: ShopItem
|
||||
onClose: () => void
|
||||
onTryLuck: (orderId: number) => void
|
||||
onConsolation: (orderId: number, rolled: number, needed: number) => void
|
||||
} = $props()
|
||||
|
||||
let activeTab = $state<'leaderboard' | 'wishlist' | 'buyers'>('leaderboard')
|
||||
|
|
@ -118,10 +120,11 @@
|
|||
credentials: 'include'
|
||||
})
|
||||
if (response.ok) {
|
||||
localHearted = !localHearted
|
||||
localHeartCount = localHearted ? localHeartCount + 1 : localHeartCount - 1
|
||||
const data = await response.json()
|
||||
localHearted = data.hearted
|
||||
localHeartCount = data.heartCount
|
||||
// Sync with the store so the shop page updates
|
||||
updateShopItemHeart(item.id, localHearted)
|
||||
updateShopItemHeart(item.id, localHearted, localHeartCount)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to toggle heart:', e)
|
||||
|
|
@ -143,12 +146,11 @@
|
|||
return
|
||||
}
|
||||
|
||||
await refreshUserScraps()
|
||||
if (data.won) {
|
||||
await refreshUserScraps()
|
||||
onTryLuck(data.orderId)
|
||||
} else {
|
||||
alertType = 'info'
|
||||
alertMessage = 'Better luck next time! You rolled ' + data.rolled + ' but needed ' + data.effectiveProbability.toFixed(0) + ' or less.'
|
||||
onConsolation(data.consolationOrderId, data.rolled, Math.floor(data.effectiveProbability))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to try luck:', e)
|
||||
|
|
@ -333,19 +335,25 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onclick={() => (showConfirmation = true)}
|
||||
disabled={item.count === 0 || tryingLuck || !canAfford}
|
||||
class="w-full px-4 py-3 bg-black text-white rounded-full font-bold hover:bg-gray-800 transition-all duration-200 disabled:opacity-50 cursor-pointer text-lg"
|
||||
>
|
||||
{#if item.count === 0}
|
||||
out of stock
|
||||
{:else if !canAfford}
|
||||
not enough scraps
|
||||
{:else}
|
||||
try your luck
|
||||
{/if}
|
||||
</button>
|
||||
{#if item.count === 0}
|
||||
<span
|
||||
class="w-full px-4 py-3 border-4 border-dashed border-gray-300 text-gray-400 rounded-full font-bold text-lg text-center cursor-not-allowed block"
|
||||
>
|
||||
sold out
|
||||
</span>
|
||||
{:else}
|
||||
<button
|
||||
onclick={() => (showConfirmation = true)}
|
||||
disabled={tryingLuck || !canAfford}
|
||||
class="w-full px-4 py-3 bg-black text-white rounded-full font-bold hover:bg-gray-800 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer text-lg"
|
||||
>
|
||||
{#if !canAfford}
|
||||
not enough scraps
|
||||
{:else}
|
||||
try your luck
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if showConfirmation}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export interface ShopItem {
|
|||
baseProbability: number
|
||||
baseUpgradeCost: number
|
||||
costMultiplier: number
|
||||
boostAmount: number
|
||||
userBoostPercent: number
|
||||
effectiveProbability: number
|
||||
}
|
||||
|
|
@ -260,14 +261,14 @@ export function updateProject(id: number, updates: Partial<Project>) {
|
|||
)
|
||||
}
|
||||
|
||||
export function updateShopItemHeart(itemId: number, hearted: boolean) {
|
||||
export function updateShopItemHeart(itemId: number, hearted: boolean, heartCount?: number) {
|
||||
shopItemsStore.update((items) =>
|
||||
items.map((item) => {
|
||||
if (item.id === itemId) {
|
||||
return {
|
||||
...item,
|
||||
userHearted: hearted,
|
||||
heartCount: hearted ? item.heartCount + 1 : item.heartCount - 1
|
||||
heartCount: heartCount ?? (hearted ? item.heartCount + 1 : item.heartCount - 1)
|
||||
}
|
||||
}
|
||||
return item
|
||||
|
|
|
|||
|
|
@ -42,9 +42,22 @@
|
|||
let formCount = $state(0)
|
||||
let formBaseProbability = $state(50)
|
||||
let formBaseUpgradeCost = $state(10)
|
||||
let formCostMultiplier = $state(115)
|
||||
let formCostMultiplier = $state(101)
|
||||
let formBoostAmount = $state(1)
|
||||
let formMonetaryValue = $state(0)
|
||||
let formError = $state<string | null>(null)
|
||||
|
||||
const PHI = (1 + Math.sqrt(5)) / 2
|
||||
const SCRAPS_PER_HOUR = PHI * 10
|
||||
const DOLLARS_PER_HOUR = 5
|
||||
const SCRAPS_PER_DOLLAR = SCRAPS_PER_HOUR / DOLLARS_PER_HOUR
|
||||
|
||||
function updateFromMonetary(value: number) {
|
||||
formMonetaryValue = value
|
||||
formPrice = Math.round(value * SCRAPS_PER_DOLLAR)
|
||||
formBaseUpgradeCost = Math.round(formPrice * 0.1) || 1
|
||||
formBaseProbability = Math.max(0.1, Math.min(100, Math.round((100 - value * 2) * 10) / 10))
|
||||
}
|
||||
let deleteConfirmId = $state<number | null>(null)
|
||||
|
||||
onMount(async () => {
|
||||
|
|
@ -79,11 +92,12 @@
|
|||
formImage = ''
|
||||
formDescription = ''
|
||||
formPrice = 0
|
||||
formMonetaryValue = 0
|
||||
formCategory = ''
|
||||
formCount = 0
|
||||
formBaseProbability = 50
|
||||
formBaseUpgradeCost = 10
|
||||
formCostMultiplier = 115
|
||||
formCostMultiplier = 101
|
||||
formBoostAmount = 1
|
||||
formError = null
|
||||
showModal = true
|
||||
|
|
@ -95,6 +109,7 @@
|
|||
formImage = item.image
|
||||
formDescription = item.description
|
||||
formPrice = item.price
|
||||
formMonetaryValue = item.price / SCRAPS_PER_DOLLAR
|
||||
formCategory = item.category
|
||||
formCount = item.count
|
||||
formBaseProbability = item.baseProbability
|
||||
|
|
@ -198,6 +213,22 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="border-4 border-black rounded-2xl p-4 mb-8">
|
||||
<h3 class="font-bold mb-3">scraps per hour reference</h3>
|
||||
<div class="grid grid-cols-4 gap-4 text-center text-sm">
|
||||
{#each [0.8, 1, 1.25, 1.5] as mult}
|
||||
<div class="p-3 bg-gray-100 rounded-lg">
|
||||
<div class="text-gray-500 mb-1">{mult}x</div>
|
||||
<div class="font-bold flex items-center justify-center gap-1">
|
||||
<Spool size={14} />
|
||||
{Math.round(SCRAPS_PER_HOUR * mult)}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">${(DOLLARS_PER_HOUR * mult).toFixed(2)}/hr</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="text-center py-12 text-gray-500">loading...</div>
|
||||
{:else if items.length === 0}
|
||||
|
|
@ -215,19 +246,19 @@
|
|||
<h3 class="font-bold text-xl">{item.name}</h3>
|
||||
<p class="text-sm text-gray-600 truncate">{item.description}</p>
|
||||
<div class="flex items-center gap-2 mt-1 text-sm flex-wrap">
|
||||
<span class="font-bold">${(item.price / SCRAPS_PER_DOLLAR).toFixed(2)}</span>
|
||||
<span class="text-gray-500">•</span>
|
||||
<span class="font-bold flex items-center gap-1"><Spool size={16} />{item.price}</span>
|
||||
{#each item.category.split(',').map(c => c.trim()).filter(Boolean) as cat}
|
||||
<span class="px-2 py-0.5 bg-gray-100 rounded-full">{cat}</span>
|
||||
{/each}
|
||||
<span class="text-gray-500">{item.count} in stock</span>
|
||||
<span class="text-gray-500">•</span>
|
||||
<span class="text-gray-500">{item.baseProbability}% base chance</span>
|
||||
<span class="text-gray-500">{item.baseProbability}%</span>
|
||||
<span class="text-gray-500">•</span>
|
||||
<span class="text-gray-500">{item.baseUpgradeCost} upgrade cost</span>
|
||||
<span class="text-gray-500">+{item.boostAmount ?? 1}%/upgrade</span>
|
||||
<span class="text-gray-500">•</span>
|
||||
<span class="text-gray-500">{item.costMultiplier / 100}x multiplier</span>
|
||||
<span class="text-gray-500">•</span>
|
||||
<span class="text-gray-500">+{item.boostAmount ?? 1}% per upgrade</span>
|
||||
<span class="text-gray-500">~{(item.price / SCRAPS_PER_HOUR).toFixed(1)} hrs</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 shrink-0">
|
||||
|
|
@ -297,17 +328,23 @@
|
|||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="monetaryValue" class="block text-sm font-bold mb-1">value ($)</label>
|
||||
<input
|
||||
id="monetaryValue"
|
||||
type="number"
|
||||
value={formMonetaryValue}
|
||||
oninput={(e) => updateFromMonetary(parseFloat(e.currentTarget.value) || 0)}
|
||||
min="0"
|
||||
step="0.01"
|
||||
class="w-full px-4 py-2 border-2 border-black rounded-lg focus:outline-none focus:border-dashed"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
= {formPrice} scraps · {formBaseProbability}% base probability · {formBaseUpgradeCost} upgrade cost · ~{(formPrice / SCRAPS_PER_HOUR).toFixed(1)} hrs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="price" class="block text-sm font-bold mb-1">price (scraps)</label>
|
||||
<input
|
||||
id="price"
|
||||
type="number"
|
||||
bind:value={formPrice}
|
||||
min="0"
|
||||
class="w-full px-4 py-2 border-2 border-black rounded-lg focus:outline-none focus:border-dashed"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="count" class="block text-sm font-bold mb-1">stock count</label>
|
||||
<input
|
||||
|
|
@ -318,17 +355,16 @@
|
|||
class="w-full px-4 py-2 border-2 border-black rounded-lg focus:outline-none focus:border-dashed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="category" class="block text-sm font-bold mb-1">categories (comma separated)</label>
|
||||
<input
|
||||
id="category"
|
||||
type="text"
|
||||
bind:value={formCategory}
|
||||
placeholder="stickers, hardware, misc"
|
||||
class="w-full px-4 py-2 border-2 border-black rounded-lg focus:outline-none focus:border-dashed"
|
||||
/>
|
||||
<div>
|
||||
<label for="category" class="block text-sm font-bold mb-1">categories</label>
|
||||
<input
|
||||
id="category"
|
||||
type="text"
|
||||
bind:value={formCategory}
|
||||
placeholder="stickers, hardware"
|
||||
class="w-full px-4 py-2 border-2 border-black rounded-lg focus:outline-none focus:border-dashed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
|
|
@ -338,8 +374,9 @@
|
|||
id="baseProbability"
|
||||
type="number"
|
||||
bind:value={formBaseProbability}
|
||||
min="0"
|
||||
min="0.1"
|
||||
max="100"
|
||||
step="0.1"
|
||||
class="w-full px-4 py-2 border-2 border-black rounded-lg focus:outline-none focus:border-dashed"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -352,13 +389,12 @@
|
|||
min="1"
|
||||
class="w-full px-4 py-2 border-2 border-black rounded-lg focus:outline-none focus:border-dashed"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">probability increase per upgrade</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="baseUpgradeCost" class="block text-sm font-bold mb-1">base upgrade cost (scraps)</label>
|
||||
<label for="baseUpgradeCost" class="block text-sm font-bold mb-1">base upgrade cost</label>
|
||||
<input
|
||||
id="baseUpgradeCost"
|
||||
type="number"
|
||||
|
|
@ -366,6 +402,7 @@
|
|||
min="0"
|
||||
class="w-full px-4 py-2 border-2 border-black rounded-lg focus:outline-none focus:border-dashed"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">auto-set to 10% of price</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="costMultiplier" class="block text-sm font-bold mb-1">cost multiplier (%)</label>
|
||||
|
|
@ -376,7 +413,7 @@
|
|||
min="100"
|
||||
class="w-full px-4 py-2 border-2 border-black rounded-lg focus:outline-none focus:border-dashed"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">115 = 1.15x cost increase per upgrade</p>
|
||||
<p class="text-xs text-gray-500 mt-1">115 = 1.15x per upgrade</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
245
frontend/src/routes/orders/+page.svelte
Normal file
245
frontend/src/routes/orders/+page.svelte
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import { ArrowLeft, Package, CheckCircle, Clock, Truck, MapPin, Origami } from '@lucide/svelte'
|
||||
import { API_URL } from '$lib/config'
|
||||
import { getUser } from '$lib/auth-client'
|
||||
|
||||
interface Order {
|
||||
id: number
|
||||
quantity: number
|
||||
pricePerItem: number
|
||||
totalPrice: number
|
||||
status: string
|
||||
orderType: string
|
||||
shippingAddress: string | null
|
||||
isFulfilled: boolean
|
||||
createdAt: string
|
||||
itemId: number
|
||||
itemName: string
|
||||
itemImage: string
|
||||
}
|
||||
|
||||
let orders = $state<Order[]>([])
|
||||
let loading = $state(true)
|
||||
let error = $state<string | null>(null)
|
||||
|
||||
onMount(async () => {
|
||||
const user = await getUser()
|
||||
if (!user) {
|
||||
goto('/')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/shop/orders`, {
|
||||
credentials: 'include'
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch orders')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
if (data.error) {
|
||||
throw new Error(data.error)
|
||||
}
|
||||
|
||||
orders = data
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load orders'
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
})
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
function getOrderTypeLabel(orderType: string): string {
|
||||
switch (orderType) {
|
||||
case 'luck_win':
|
||||
return 'won'
|
||||
case 'consolation':
|
||||
return 'consolation'
|
||||
case 'purchase':
|
||||
return 'purchased'
|
||||
default:
|
||||
return orderType
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusColor(status: string, isFulfilled: boolean): string {
|
||||
if (isFulfilled) return 'bg-green-100 text-green-700 border-green-600'
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'bg-yellow-100 text-yellow-700 border-yellow-600'
|
||||
case 'shipped':
|
||||
return 'bg-blue-100 text-blue-700 border-blue-600'
|
||||
case 'delivered':
|
||||
return 'bg-green-100 text-green-700 border-green-600'
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-700 border-gray-600'
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusIcon(status: string, isFulfilled: boolean) {
|
||||
if (isFulfilled) return CheckCircle
|
||||
switch (status) {
|
||||
case 'shipped':
|
||||
return Truck
|
||||
case 'delivered':
|
||||
return CheckCircle
|
||||
default:
|
||||
return Clock
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusLabel(status: string, isFulfilled: boolean): string {
|
||||
if (isFulfilled) return 'fulfilled'
|
||||
return status
|
||||
}
|
||||
|
||||
interface ParsedAddress {
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
address1?: string
|
||||
address2?: string | null
|
||||
city?: string
|
||||
state?: string
|
||||
postalCode?: string
|
||||
country?: string
|
||||
phone?: string
|
||||
}
|
||||
|
||||
function parseAddress(addressJson: string): ParsedAddress | null {
|
||||
try {
|
||||
return JSON.parse(addressJson)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function formatAddress(addressJson: string): string {
|
||||
const addr = parseAddress(addressJson)
|
||||
if (!addr) return addressJson
|
||||
|
||||
const parts: string[] = []
|
||||
if (addr.firstName || addr.lastName) {
|
||||
parts.push([addr.firstName, addr.lastName].filter(Boolean).join(' '))
|
||||
}
|
||||
if (addr.address1) parts.push(addr.address1)
|
||||
if (addr.address2) parts.push(addr.address2)
|
||||
if (addr.city || addr.state || addr.postalCode) {
|
||||
const cityLine = [addr.city, addr.state].filter(Boolean).join(', ')
|
||||
parts.push([cityLine, addr.postalCode].filter(Boolean).join(' '))
|
||||
}
|
||||
if (addr.country) parts.push(addr.country)
|
||||
|
||||
return parts.join(', ')
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>my orders - scraps</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="pt-24 px-6 md:px-12 max-w-4xl mx-auto pb-24">
|
||||
<a
|
||||
href="/shop"
|
||||
class="inline-flex items-center gap-2 mb-8 font-bold hover:underline cursor-pointer"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
back to shop
|
||||
</a>
|
||||
|
||||
<h1 class="text-4xl md:text-5xl font-bold mb-8">my orders</h1>
|
||||
|
||||
{#if loading}
|
||||
<div class="text-center py-12 text-gray-500">loading orders...</div>
|
||||
{:else if error}
|
||||
<div class="text-center py-12 text-red-600">{error}</div>
|
||||
{:else if orders.length === 0}
|
||||
<div class="border-4 border-dashed border-gray-300 rounded-2xl p-12 text-center">
|
||||
<Package size={48} class="mx-auto text-gray-400 mb-4" />
|
||||
<p class="text-gray-500 text-lg">no orders yet</p>
|
||||
<p class="text-gray-400 text-sm mt-2">try your luck in the shop to get some goodies!</p>
|
||||
<a
|
||||
href="/shop"
|
||||
class="inline-block mt-6 px-6 py-3 bg-black text-white rounded-full font-bold hover:bg-gray-800 transition-all duration-200 cursor-pointer"
|
||||
>
|
||||
go to shop
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
{#each orders as order}
|
||||
{@const StatusIcon = getStatusIcon(order.status, order.isFulfilled)}
|
||||
{@const isConsolation = order.orderType === 'consolation'}
|
||||
<div class="border-4 border-black rounded-2xl p-6 hover:border-dashed transition-all duration-200">
|
||||
<div class="flex gap-4">
|
||||
{#if isConsolation}
|
||||
<div class="w-20 h-20 rounded-lg border-2 border-black bg-yellow-50 shrink-0 flex items-center justify-center">
|
||||
<Origami size={40} class="text-yellow-600" />
|
||||
</div>
|
||||
{:else}
|
||||
<img
|
||||
src={order.itemImage}
|
||||
alt={order.itemName}
|
||||
class="w-20 h-20 object-contain rounded-lg border-2 border-black bg-gray-50 shrink-0"
|
||||
/>
|
||||
{/if}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
{#if isConsolation}
|
||||
<h3 class="font-bold text-xl">paper scraps</h3>
|
||||
<p class="text-sm text-gray-400 line-through">{order.itemName}</p>
|
||||
{:else}
|
||||
<h3 class="font-bold text-xl">{order.itemName}</h3>
|
||||
{/if}
|
||||
<p class="text-sm text-gray-500">
|
||||
{getOrderTypeLabel(order.orderType)} · {formatDate(order.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
class="shrink-0 px-3 py-1 rounded-full text-sm font-bold border-2 flex items-center gap-1 {getStatusColor(order.status, order.isFulfilled)}"
|
||||
>
|
||||
<StatusIcon size={14} />
|
||||
{getStatusLabel(order.status, order.isFulfilled)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-4 text-sm">
|
||||
<span class="text-gray-600">
|
||||
<span class="font-bold">{order.totalPrice}</span> scraps
|
||||
</span>
|
||||
{#if order.quantity > 1}
|
||||
<span class="text-gray-600">qty: {order.quantity}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if order.shippingAddress}
|
||||
<div class="mt-3 flex items-start gap-2 text-sm text-gray-600">
|
||||
<MapPin size={16} class="shrink-0 mt-0.5" />
|
||||
<span class="break-words">{formatAddress(order.shippingAddress)}</span>
|
||||
</div>
|
||||
{:else if !order.isFulfilled}
|
||||
<p class="mt-3 text-sm text-yellow-600 font-bold">
|
||||
⚠️ no shipping address provided
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
1
frontend/src/routes/orders/+page.ts
Normal file
1
frontend/src/routes/orders/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const ssr = false
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import { ArrowLeft, Pencil, Send, Clock, CheckCircle, XCircle, AlertCircle, Github, AlertTriangle, PlaneTakeoff, Plus, Globe } from '@lucide/svelte'
|
||||
import { ArrowLeft, Pencil, Send, Clock, CheckCircle, XCircle, AlertCircle, Github, AlertTriangle, PlaneTakeoff, Plus, Globe, Spool } from '@lucide/svelte'
|
||||
import { getUser } from '$lib/auth-client'
|
||||
import { API_URL } from '$lib/config'
|
||||
import { formatHours } from '$lib/utils'
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
hours: number
|
||||
hoursOverride?: number | null
|
||||
status: string
|
||||
scrapsAwarded: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
|
@ -32,7 +33,7 @@
|
|||
}
|
||||
|
||||
interface ActivityEntry {
|
||||
type: 'review' | 'created' | 'submitted'
|
||||
type: 'review' | 'created' | 'submitted' | 'scraps_earned'
|
||||
action?: string
|
||||
feedbackForAuthor?: string | null
|
||||
createdAt: string
|
||||
|
|
@ -215,6 +216,12 @@
|
|||
<Clock size={18} />
|
||||
{formatHours(project.hours)}h
|
||||
</span>
|
||||
{#if project.scrapsAwarded > 0}
|
||||
<span class="px-4 py-2 bg-green-100 text-green-700 rounded-full font-bold border-4 border-green-600 flex items-center gap-2">
|
||||
<Spool size={18} />
|
||||
+{project.scrapsAwarded} scraps earned
|
||||
</span>
|
||||
{/if}
|
||||
{#if project.githubUrl}
|
||||
<a
|
||||
href={project.githubUrl}
|
||||
|
|
@ -365,6 +372,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if entry.type === 'scraps_earned'}
|
||||
<div class="relative flex items-center gap-3 ml-8 py-2">
|
||||
<div class="absolute left-[-26px] w-6 h-6 bg-white rounded-full flex items-center justify-center z-10">
|
||||
<Spool size={16} class="text-green-600" />
|
||||
</div>
|
||||
<span class="text-sm text-green-600 font-bold">{entry.action} · {formatDate(entry.createdAt)}</span>
|
||||
</div>
|
||||
{:else if entry.type === 'submitted'}
|
||||
<div class="relative flex items-center gap-3 ml-8 py-2">
|
||||
<div class="absolute left-[-26px] w-6 h-6 bg-white rounded-full flex items-center justify-center z-10">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { ArrowLeft, Github, Clock, CheckCircle, AlertTriangle, Package } from '@lucide/svelte'
|
||||
import { ArrowLeft, Github, Globe, Clock, CheckCircle, AlertTriangle, Package } from '@lucide/svelte'
|
||||
import { API_URL } from '$lib/config'
|
||||
import { formatHours } from '$lib/utils'
|
||||
import { getUser } from '$lib/auth-client'
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
description: string
|
||||
image: string | null
|
||||
githubUrl: string | null
|
||||
playableUrl: string | null
|
||||
hours: number
|
||||
status: string
|
||||
createdAt: string
|
||||
|
|
@ -124,22 +125,45 @@
|
|||
</span>
|
||||
</div>
|
||||
|
||||
{#if project.githubUrl}
|
||||
<a
|
||||
href={project.githubUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-2 mt-4 px-4 py-2 border-4 border-black rounded-full font-bold hover:border-dashed transition-all duration-200 cursor-pointer"
|
||||
>
|
||||
<Github size={18} />
|
||||
<span>view on github</span>
|
||||
</a>
|
||||
{/if}
|
||||
<div class="flex flex-wrap gap-3 mt-4">
|
||||
{#if project.githubUrl}
|
||||
<a
|
||||
href={project.githubUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 border-4 border-black rounded-full font-bold hover:border-dashed transition-all duration-200 cursor-pointer"
|
||||
>
|
||||
<Github size={18} />
|
||||
<span>view on github</span>
|
||||
</a>
|
||||
{:else}
|
||||
<span class="inline-flex items-center gap-2 px-4 py-2 border-4 border-dashed border-gray-300 text-gray-400 rounded-full font-bold cursor-not-allowed">
|
||||
<Github size={18} />
|
||||
<span>view on github</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if project.playableUrl}
|
||||
<a
|
||||
href={project.playableUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 border-4 border-black rounded-full font-bold hover:border-dashed transition-all duration-200 cursor-pointer"
|
||||
>
|
||||
<Globe size={18} />
|
||||
<span>try it out</span>
|
||||
</a>
|
||||
{:else}
|
||||
<span class="inline-flex items-center gap-2 px-4 py-2 border-4 border-dashed border-gray-300 text-gray-400 rounded-full font-bold cursor-not-allowed">
|
||||
<Globe size={18} />
|
||||
<span>try it out</span>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isOwner}
|
||||
<a
|
||||
href="/projects/{project.id}"
|
||||
class="inline-flex items-center gap-2 mt-4 ml-2 px-4 py-2 bg-black text-white border-4 border-black rounded-full font-bold hover:bg-gray-800 transition-all duration-200 cursor-pointer"
|
||||
class="inline-flex items-center gap-2 mt-4 px-4 py-2 bg-black text-white border-4 border-black rounded-full font-bold hover:bg-gray-800 transition-all duration-200 cursor-pointer"
|
||||
>
|
||||
edit project
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@
|
|||
{#if upgrading === item.id}
|
||||
upgrading...
|
||||
{:else}
|
||||
upgrade +1% ({nextCost} scraps)
|
||||
upgrade +{item.boostAmount}% ({nextCost} scraps)
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import AddressSelectModal from '$lib/components/AddressSelectModal.svelte'
|
||||
import { API_URL } from '$lib/config'
|
||||
import { getUser } from '$lib/auth-client'
|
||||
import { X, Spool } from '@lucide/svelte'
|
||||
import { X, Spool, PackageCheck } from '@lucide/svelte'
|
||||
import {
|
||||
shopItemsStore,
|
||||
shopLoading,
|
||||
|
|
@ -22,6 +22,10 @@
|
|||
let winningItemName = $state<string | null>(null)
|
||||
let pendingOrders = $state<{ orderId: number; itemName: string }[]>([])
|
||||
|
||||
let consolationOrderId = $state<number | null>(null)
|
||||
let consolationRolled = $state<number | null>(null)
|
||||
let consolationNeeded = $state<number | null>(null)
|
||||
|
||||
let categories = $derived.by(() => {
|
||||
const allCategories = new Set<string>()
|
||||
$shopItemsStore.forEach((item) => {
|
||||
|
|
@ -110,6 +114,13 @@
|
|||
selectedItem = null
|
||||
}
|
||||
|
||||
function handleConsolation(orderId: number, rolled: number, needed: number) {
|
||||
consolationOrderId = orderId
|
||||
consolationRolled = rolled
|
||||
consolationNeeded = needed
|
||||
selectedItem = null
|
||||
}
|
||||
|
||||
function handleAddressComplete() {
|
||||
fetchShopItems(true)
|
||||
winningOrderId = null
|
||||
|
|
@ -129,16 +140,14 @@
|
|||
})
|
||||
|
||||
async function toggleHeart(itemId: number) {
|
||||
const item = $shopItemsStore.find((i) => i.id === itemId)
|
||||
if (!item) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/shop/items/${itemId}/heart`, {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
})
|
||||
if (response.ok) {
|
||||
updateShopItemHeart(itemId, !item.userHearted)
|
||||
const data = await response.json()
|
||||
updateShopItemHeart(itemId, data.hearted, data.heartCount)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle heart:', error)
|
||||
|
|
@ -227,9 +236,16 @@
|
|||
{#each sortedItems as item (item.id)}
|
||||
<button
|
||||
onclick={() => (selectedItem = item)}
|
||||
class="border-4 border-black rounded-2xl p-4 hover:border-dashed transition-all cursor-pointer text-left"
|
||||
class="border-4 border-black rounded-2xl p-4 hover:border-dashed transition-all cursor-pointer text-left relative overflow-hidden {item.count === 0 ? 'bg-gray-100' : ''}"
|
||||
>
|
||||
<div class="relative">
|
||||
{#if item.count === 0}
|
||||
<div class="absolute top-0 right-0 z-20">
|
||||
<div class="bg-red-600 text-white text-xs font-bold px-8 py-1 transform rotate-45 translate-x-6 translate-y-3 shadow-md">
|
||||
sold out
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="relative {item.count === 0 ? 'opacity-50 grayscale' : ''}">
|
||||
<img src={item.image} alt={item.name} class="w-full h-32 object-contain mb-4" />
|
||||
<span
|
||||
class="absolute top-0 right-0 text-xs font-bold px-2 py-1 rounded-full {getProbabilityBgColor(item.effectiveProbability)} {getProbabilityColor(item.effectiveProbability)}"
|
||||
|
|
@ -237,26 +253,28 @@
|
|||
{item.effectiveProbability.toFixed(0)}% chance
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="font-bold text-xl mb-1">{item.name}</h3>
|
||||
<p class="text-sm text-gray-600 mb-2">{item.description}</p>
|
||||
<div class="mb-3">
|
||||
<span class="text-lg font-bold flex items-center gap-1"><Spool size={18} />{item.price}</span>
|
||||
<div class="flex gap-1 flex-wrap mt-2">
|
||||
{#each item.category.split(',').map((c) => c.trim()).filter(Boolean) as cat}
|
||||
<span class="text-xs px-2 py-1 bg-gray-100 rounded-full">{cat}</span>
|
||||
{/each}
|
||||
<div class={item.count === 0 ? 'opacity-50' : ''}>
|
||||
<h3 class="font-bold text-xl mb-1">{item.name}</h3>
|
||||
<p class="text-sm text-gray-600 mb-2">{item.description}</p>
|
||||
<div class="mb-3">
|
||||
<span class="text-lg font-bold flex items-center gap-1"><Spool size={18} />{item.price}</span>
|
||||
<div class="flex gap-1 flex-wrap mt-2">
|
||||
{#each item.category.split(',').map((c) => c.trim()).filter(Boolean) as cat}
|
||||
<span class="text-xs px-2 py-1 bg-gray-100 rounded-full">{cat}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs {item.count === 0 ? 'text-red-500 font-bold' : 'text-gray-500'}">{item.count === 0 ? 'sold out' : `${item.count} left`}</span>
|
||||
<HeartButton
|
||||
count={item.heartCount}
|
||||
hearted={item.userHearted}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleHeart(item.id)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500">{item.count} left</span>
|
||||
<HeartButton
|
||||
count={item.heartCount}
|
||||
hearted={item.userHearted}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleHeart(item.id)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
|
|
@ -269,6 +287,7 @@
|
|||
item={selectedItem}
|
||||
onClose={() => (selectedItem = null)}
|
||||
onTryLuck={handleTryLuck}
|
||||
onConsolation={handleConsolation}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
|
@ -283,3 +302,40 @@
|
|||
onComplete={handleAddressComplete}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if consolationOrderId}
|
||||
<AddressSelectModal
|
||||
orderId={consolationOrderId}
|
||||
itemName="consolation scrap paper"
|
||||
onClose={() => {
|
||||
consolationOrderId = null
|
||||
consolationRolled = null
|
||||
consolationNeeded = null
|
||||
}}
|
||||
onComplete={() => {
|
||||
consolationOrderId = null
|
||||
consolationRolled = null
|
||||
consolationNeeded = null
|
||||
}}
|
||||
>
|
||||
{#snippet header()}
|
||||
<div class="mb-4 p-4 bg-yellow-50 border-2 border-yellow-400 rounded-xl">
|
||||
<p class="font-bold text-yellow-800">better luck next time!</p>
|
||||
<p class="text-sm text-yellow-700 mt-1">
|
||||
you rolled {consolationRolled} but needed {consolationNeeded} or less.
|
||||
</p>
|
||||
<p class="text-sm text-yellow-700 mt-2">
|
||||
as a consolation, we'll send you a random scrap of paper from hack club hq! just tell us where to ship it.
|
||||
</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
</AddressSelectModal>
|
||||
{/if}
|
||||
|
||||
<a
|
||||
href="/orders"
|
||||
class="fixed bottom-6 right-6 z-50 flex items-center gap-2 px-6 py-3 bg-black text-white rounded-full font-bold hover:bg-gray-800 transition-all duration-200 border-4 border-black cursor-pointer"
|
||||
>
|
||||
<PackageCheck size={20} />
|
||||
<span>my orders</span>
|
||||
</a>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue