diff --git a/CardListing/33cf8860-c023-4d29-978b-aa51259a191c.json b/CardListing/33cf8860-c023-4d29-978b-aa51259a191c.json new file mode 100644 index 0000000..6bb6640 --- /dev/null +++ b/CardListing/33cf8860-c023-4d29-978b-aa51259a191c.json @@ -0,0 +1,66 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "CardListing", + "module": "../catalog-app/listing/listing" + } + }, + "type": "card", + "attributes": { + "name": "Golf Scorecard", + "images": [ + "https://boxel-images.boxel.ai/app-assets/catalog/golf-scorecard-listing/screenshot_01.png" + ], + "summary": "This card logs strokes, pars, and putts across all 18 holes\u2014auto-computing birdies, bogeys, and total scores. Includes live leaderboard views, TV-style overlays, and PGA/USGA branding support. Ideal for tournaments, sports apps, and golf analytics dashboards.", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": "https://boxel-images.boxel.ai/app-assets/catalog/golf-scorecard-listing/thumbnail.png" + } + }, + "relationships": { + "tags": { + "links": { + "self": null + } + }, + "skills": { + "links": { + "self": null + } + }, + "license": { + "links": { + "self": null + } + }, + "specs.0": { + "links": { + "self": "../Spec/6733cf88-60c0-436d-a9d7-8baa51259a19" + } + }, + "publisher": { + "links": { + "self": "../Publisher/9d3ca05b-684b-408f-8d8c-dd353d9956e0" + } + }, + "examples.0": { + "links": { + "self": "../golf-scorecard/GolfScorecard/428b992e-9e00-42ae-976b-ae08bcd73e4e" + } + }, + "categories.0": { + "links": { + "self": "../Category/sports-fitness" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/Spec/6733cf88-60c0-436d-a9d7-8baa51259a19.json b/Spec/6733cf88-60c0-436d-a9d7-8baa51259a19.json new file mode 100644 index 0000000..0500ef4 --- /dev/null +++ b/Spec/6733cf88-60c0-436d-a9d7-8baa51259a19.json @@ -0,0 +1,30 @@ +{ + "data": { + "type": "card", + "attributes": { + "readMe": null, + "ref": { + "name": "GolfScorecard", + "module": "../golf-scorecard/golf-scorecard" + }, + "specType": "card", + "containedExamples": [], + "cardTitle": "GolfScorecard", + "cardDescription": "Spec of GolfScorecard", + "cardThumbnailURL": null + }, + "relationships": { + "linkedExamples": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "https://cardstack.com/base/spec", + "name": "Spec" + } + } + } +} \ No newline at end of file diff --git a/golf-scorecard/GolfScorecard/428b992e-9e00-42ae-976b-ae08bcd73e4e.json b/golf-scorecard/GolfScorecard/428b992e-9e00-42ae-976b-ae08bcd73e4e.json new file mode 100644 index 0000000..6be8573 --- /dev/null +++ b/golf-scorecard/GolfScorecard/428b992e-9e00-42ae-976b-ae08bcd73e4e.json @@ -0,0 +1,151 @@ +{ + "data": { + "type": "card", + "attributes": { + "tournamentName": "US Open Championship", + "courseName": "Pinehurst No. 2", + "playerName": "Rory McIlroy", + "roundDate": "2025-07-14", + "roundNumber": 1, + "holes": [ + { + "holeNumber": 1, + "par": 4, + "yards": 445, + "strokes": 4, + "putts": 2 + }, + { + "holeNumber": 2, + "par": 4, + "yards": 411, + "strokes": 3, + "putts": 1 + }, + { + "holeNumber": 3, + "par": 3, + "yards": 198, + "strokes": 3, + "putts": 2 + }, + { + "holeNumber": 4, + "par": 4, + "yards": 381, + "strokes": 4, + "putts": 1 + }, + { + "holeNumber": 5, + "par": 5, + "yards": 567, + "strokes": 4, + "putts": 2 + }, + { + "holeNumber": 6, + "par": 4, + "yards": 450, + "strokes": 5, + "putts": 2 + }, + { + "holeNumber": 7, + "par": 3, + "yards": 176, + "strokes": 2, + "putts": 1 + }, + { + "holeNumber": 8, + "par": 4, + "yards": 422, + "strokes": 4, + "putts": 2 + }, + { + "holeNumber": 9, + "par": 4, + "yards": 436, + "strokes": 4, + "putts": 2 + }, + { + "holeNumber": 10, + "par": 4, + "yards": 447, + "strokes": 4, + "putts": 2 + }, + { + "holeNumber": 11, + "par": 5, + "yards": 551, + "strokes": 5, + "putts": 2 + }, + { + "holeNumber": 12, + "par": 3, + "yards": 164, + "strokes": 3, + "putts": 1 + }, + { + "holeNumber": 13, + "par": 4, + "yards": 408, + "strokes": 4, + "putts": 2 + }, + { + "holeNumber": 14, + "par": 4, + "yards": 458, + "strokes": 4, + "putts": 2 + }, + { + "holeNumber": 15, + "par": 3, + "yards": 227, + "strokes": 2, + "putts": 1 + }, + { + "holeNumber": 16, + "par": 4, + "yards": 479, + "strokes": 4, + "putts": 2 + }, + { + "holeNumber": 17, + "par": 4, + "yards": 429, + "strokes": 3, + "putts": 1 + }, + { + "holeNumber": 18, + "par": 5, + "yards": 543, + "strokes": 5, + "putts": 2 + } + ], + "signedByPlayer": false, + "signedByMarker": false, + "cardTitle": "Rory McIlroy - 2025 US Open Round 1", + "cardDescription": "First round scorecard for Rory McIlroy at the 2025 US Open Championship", + "cardThumbnailURL": null + }, + "meta": { + "adoptsFrom": { + "module": "../golf-scorecard", + "name": "GolfScorecard" + } + } + } +} \ No newline at end of file diff --git a/golf-scorecard/golf-scorecard.gts b/golf-scorecard/golf-scorecard.gts new file mode 100644 index 0000000..2d73109 --- /dev/null +++ b/golf-scorecard/golf-scorecard.gts @@ -0,0 +1,1316 @@ +import { + CardDef, + FieldDef, + Component, + field, + contains, + containsMany, +} from 'https://cardstack.com/base/card-api'; +import StringField from 'https://cardstack.com/base/string'; +import NumberField from 'https://cardstack.com/base/number'; +import DateField from 'https://cardstack.com/base/date'; +import BooleanField from 'https://cardstack.com/base/boolean'; +import { cn } from '@cardstack/boxel-ui/helpers'; +import TrophyIcon from '@cardstack/boxel-icons/trophy'; + +export class HoleField extends FieldDef { + static displayName = 'Hole'; + @field holeNumber = contains(NumberField); + @field par = contains(NumberField); + @field yards = contains(NumberField); + @field strokes = contains(NumberField); + @field putts = contains(NumberField); + + @field score = contains(StringField, { + computeVia: function (this: HoleField) { + try { + const strokes = this.strokes ?? 0; + const par = this.par ?? 4; + const diff = strokes - par; + + if (strokes === 1) return 'Hole-in-One'; + if (diff === -3) return 'Albatross'; + if (diff === -2) return 'Eagle'; + if (diff === -1) return 'Birdie'; + if (diff === 0) return 'Par'; + if (diff === 1) return 'Bogey'; + if (diff === 2) return 'Double Bogey'; + return `+${diff}`; + } catch (e) { + console.error('Error computing score:', e); + return 'Par'; + } + }, + }); +} + +export class GolfScorecard extends CardDef { + static displayName = 'Golf Scorecard'; + static icon = TrophyIcon; + static prefersWideFormat = true; + + @field tournamentName = contains(StringField); + @field courseName = contains(StringField); + @field playerName = contains(StringField); + @field roundDate = contains(DateField); + @field roundNumber = contains(NumberField); + @field holes = containsMany(HoleField); + @field signedByPlayer = contains(BooleanField); + @field signedByMarker = contains(BooleanField); + + @field frontNineTotal = contains(NumberField, { + computeVia: function (this: GolfScorecard) { + try { + if (!this.holes || !Array.isArray(this.holes)) return 0; + return this.holes + .slice(0, 9) + .reduce((total, hole) => total + (hole.strokes ?? 0), 0); + } catch (e) { + console.error('Error computing front nine total:', e); + return 0; + } + }, + }); + + @field backNineTotal = contains(NumberField, { + computeVia: function (this: GolfScorecard) { + try { + if (!this.holes || !Array.isArray(this.holes)) return 0; + return this.holes + .slice(9, 18) + .reduce((total, hole) => total + (hole.strokes ?? 0), 0); + } catch (e) { + console.error('Error computing back nine total:', e); + return 0; + } + }, + }); + + @field totalScore = contains(NumberField, { + computeVia: function (this: GolfScorecard) { + try { + return this.frontNineTotal + this.backNineTotal; + } catch (e) { + console.error('Error computing total score:', e); + return 0; + } + }, + }); + + @field scoreToPar = contains(StringField, { + computeVia: function (this: GolfScorecard) { + try { + if (!this.holes || !Array.isArray(this.holes)) return 'E'; + const totalPar = this.holes.reduce( + (total, hole) => total + (hole.par ?? 4), + 0, + ); + const diff = this.totalScore - totalPar; + + if (diff === 0) return 'E'; + if (diff > 0) return `+${diff}`; + return `${diff}`; + } catch (e) { + console.error('Error computing score to par:', e); + return 'E'; + } + }, + }); + + constructor(data?: Record | undefined) { + super(data); + + this.tournamentName = 'US Open Championship'; + this.roundDate = new Date(); + this.roundNumber = 1; + this.signedByPlayer = false; + this.signedByMarker = false; + + // Initialize 18 holes with US Open typical setup + const defaultHoles = [ + { holeNumber: 1, par: 4, yards: 445 }, + { holeNumber: 2, par: 4, yards: 411 }, + { holeNumber: 3, par: 3, yards: 198 }, + { holeNumber: 4, par: 4, yards: 381 }, + { holeNumber: 5, par: 5, yards: 567 }, + { holeNumber: 6, par: 4, yards: 450 }, + { holeNumber: 7, par: 3, yards: 176 }, + { holeNumber: 8, par: 4, yards: 422 }, + { holeNumber: 9, par: 4, yards: 436 }, + { holeNumber: 10, par: 4, yards: 447 }, + { holeNumber: 11, par: 5, yards: 551 }, + { holeNumber: 12, par: 3, yards: 164 }, + { holeNumber: 13, par: 4, yards: 408 }, + { holeNumber: 14, par: 4, yards: 458 }, + { holeNumber: 15, par: 3, yards: 227 }, + { holeNumber: 16, par: 4, yards: 479 }, + { holeNumber: 17, par: 4, yards: 429 }, + { holeNumber: 18, par: 5, yards: 543 }, + ]; + + // Create proper HoleField instances + this.holes = defaultHoles.map((holeData) => { + const hole = new HoleField({}); + hole.holeNumber = holeData.holeNumber; + hole.par = holeData.par; + hole.yards = holeData.yards; + hole.strokes = 0; + hole.putts = 0; + return hole; + }); + } + + static embedded = class Embedded extends Component { + get formattedRoundDate() { + let date = this.args.model?.roundDate; + if (!date) return ''; + if (typeof date === 'string') date = new Date(date); + return date instanceof Date && !isNaN(date.getTime()) + ? date.toLocaleDateString() + : ''; + } + + + get scoreClass() { + try { + const scoreToPar = this.args.model?.scoreToPar; + if (scoreToPar === 'E') return 'even-par'; + if (scoreToPar && scoreToPar.startsWith('-')) return 'under-par'; + if (scoreToPar && scoreToPar.startsWith('+')) return 'over-par'; + return 'even-par'; + } catch (e) { + return 'even-par'; + } + } + + get totalPar() { + try { + if (!this.args.model?.holes) return 72; + return this.args.model.holes.reduce( + (total, hole) => total + (hole.par || 0), + 0, + ); + } catch (e) { + return 72; + } + } + }; + + static isolated = class Isolated extends Component { + get formattedRoundDate() { + let date = this.args.model?.roundDate; + if (!date) return ''; + if (typeof date === 'string') date = new Date(date); + return date instanceof Date && !isNaN(date.getTime()) + ? date.toLocaleDateString() + : ''; + } + + get frontNinePar() { + try { + if (!this.args.model?.holes) return 0; + return this.args.model.holes + .slice(0, 9) + .reduce((total, hole) => total + (hole.par ?? 0), 0); + } catch (e) { + return 0; + } + } + + get backNinePar() { + try { + if (!this.args.model?.holes) return 0; + return this.args.model.holes + .slice(9, 18) + .reduce((total, hole) => total + (hole.par ?? 0), 0); + } catch (e) { + return 0; + } + } + + get frontNineYards() { + try { + if (!this.args.model?.holes) return 0; + return this.args.model.holes + .slice(0, 9) + .reduce((total, hole) => total + (hole.yards ?? 0), 0); + } catch (e) { + return 0; + } + } + + get backNineYards() { + try { + if (!this.args.model?.holes) return 0; + return this.args.model.holes + .slice(9, 18) + .reduce((total, hole) => total + (hole.yards ?? 0), 0); + } catch (e) { + return 0; + } + } + + get frontNinePutts() { + try { + if (!this.args.model?.holes) return 0; + return this.args.model.holes + .slice(0, 9) + .reduce((total, hole) => total + (hole.putts ?? 0), 0); + } catch (e) { + return 0; + } + } + + get backNinePutts() { + try { + if (!this.args.model?.holes) return 0; + return this.args.model.holes + .slice(9, 18) + .reduce((total, hole) => total + (hole.putts ?? 0), 0); + } catch (e) { + return 0; + } + } + + + }; + + static edit = class Edit extends Component { + + }; +} + +function getScoreClass(score: string) { + if (!score) return ''; + return score.toLowerCase().replace(/[^a-z]/g, '_'); +} + +function slice(array: any[], start: number, end: number) { + if (!array || !Array.isArray(array)) return []; + return array.slice(start, end); +}