diff --git a/CardListing/e6420987-6c85-48b5-93f0-6051d66c643e.json b/CardListing/e6420987-6c85-48b5-93f0-6051d66c643e.json
new file mode 100644
index 0000000..f14878d
--- /dev/null
+++ b/CardListing/e6420987-6c85-48b5-93f0-6051d66c643e.json
@@ -0,0 +1,69 @@
+{
+ "data": {
+ "meta": {
+ "adoptsFrom": {
+ "name": "CardListing",
+ "module": "https://realms-staging.stack.cards/catalog/catalog-app/listing/listing"
+ }
+ },
+ "type": "card",
+ "attributes": {
+ "name": "Golf Scorecard",
+ "images": [],
+ "summary": "The GolfScorecard component provides a structured digital record of a golf round, including details such as tournament, course, player, and date information. It encapsulates the scoring details for 18 holes, tracking strokes, par, yardage, and special scores like hole-in-one or eagle. The component calculates aggregate scores for the front nine, back nine, total strokes, and score relative to par. It offers embedded or isolated views with detailed score breakdowns, including per-hole scores and overall summaries, and includes signature fields for validation. Its primary purpose is to facilitate comprehensive recording and display of golf round scores in a clear, visual format suitable for both digital and print use.",
+ "cardInfo": {
+ "name": null,
+ "notes": null,
+ "summary": null,
+ "cardThumbnailURL": null
+ }
+ },
+ "relationships": {
+ "specs.0": {
+ "links": {
+ "self": "../Spec/8ea2e332-6a6c-4ae9-9d99-02a133a9ab5b"
+ }
+ },
+ "skills": {
+ "links": {
+ "self": null
+ }
+ },
+ "tags.0": {
+ "links": {
+ "self": "https://realms-staging.stack.cards/catalog/Tag/51de249c-516a-4c4d-bd88-76e88274c483"
+ }
+ },
+ "license": {
+ "links": {
+ "self": "https://realms-staging.stack.cards/catalog/License/4c5a023b-a72c-4f90-930b-da60a1de5b2d"
+ }
+ },
+ "publisher": {
+ "links": {
+ "self": null
+ }
+ },
+ "examples.0": {
+ "links": {
+ "self": "../golf-scorecard-0ed3ac0e-4b0d-452b-b17d-4cb63cecb223/GolfScorecard/ee5723a6-256c-4a03-b57e-b542a89553b9"
+ }
+ },
+ "categories.0": {
+ "links": {
+ "self": "https://realms-staging.stack.cards/catalog/Category/sports-fitness"
+ }
+ },
+ "cardInfo.theme": {
+ "links": {
+ "self": null
+ }
+ },
+ "cardInfo.cardThumbnail": {
+ "links": {
+ "self": null
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Spec/8ea2e332-6a6c-4ae9-9d99-02a133a9ab5b.json b/Spec/8ea2e332-6a6c-4ae9-9d99-02a133a9ab5b.json
new file mode 100644
index 0000000..ed666cd
--- /dev/null
+++ b/Spec/8ea2e332-6a6c-4ae9-9d99-02a133a9ab5b.json
@@ -0,0 +1,45 @@
+{
+ "data": {
+ "meta": {
+ "adoptsFrom": {
+ "name": "Spec",
+ "module": "https://cardstack.com/base/spec"
+ }
+ },
+ "type": "card",
+ "attributes": {
+ "ref": {
+ "name": "GolfScorecard",
+ "module": "../golf-scorecard-0ed3ac0e-4b0d-452b-b17d-4cb63cecb223/golf-scorecard"
+ },
+ "readMe": "# Golf Scorecard\n\n## Summary\nThe `GolfScorecard` card definition represents a golf scorecard, including information about the tournament, course, player, and hole-by-hole scores. It provides computed fields for various totals and scoring metrics.\n\n## Import\n```javascript\nimport { GolfScorecard, HoleField } from 'https://realms-staging.stack.cards/chuan16/catalog-listing-test-1/golf-scorecard-0ed3ac0e-4b0d-452b-b17d-4cb63cecb223/golf-scorecard';\n```\n\n## Usage as a Field\nYou can use the `GolfScorecard` card definition as a field within a consuming card or field:\n\n```javascript\nimport { CardDef, FieldDef, field, contains, containsMany } from 'https://cardstack.com/base/card-api';\n\nexport class MyCard extends CardDef {\n @field golfScorecard = contains(GolfScorecard);\n}\n```\n\n## Template Usage\nInside the template of a consuming card or field, you can access the `GolfScorecard` instance and its fields:\n\n```html\n
\n
{{@model.golfScorecard.tournamentName}}
\n
Player: {{@model.golfScorecard.playerName}}
\n
Total Score: {{@model.golfScorecard.totalScore}}
\n
Score to Par: {{@model.golfScorecard.scoreToPar}}
\n
\n```\n\nYou can also use the `Embedded` and `Edit` components provided by the `GolfScorecard` definition to render the scorecard in different views:\n\n```html\n<@model.golfScorecard.Embedded />\n<@model.golfScorecard.Edit />\n```",
+ "cardInfo": {
+ "name": null,
+ "notes": null,
+ "summary": null,
+ "cardThumbnailURL": null
+ },
+ "specType": "card",
+ "cardTitle": "Golf Scorecard",
+ "cardDescription": null,
+ "containedExamples": []
+ },
+ "relationships": {
+ "cardInfo.theme": {
+ "links": {
+ "self": null
+ }
+ },
+ "linkedExamples": {
+ "links": {
+ "self": null
+ }
+ },
+ "cardInfo.cardThumbnail": {
+ "links": {
+ "self": null
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/golf-scorecard-0ed3ac0e-4b0d-452b-b17d-4cb63cecb223/GolfScorecard/ee5723a6-256c-4a03-b57e-b542a89553b9.json b/golf-scorecard-0ed3ac0e-4b0d-452b-b17d-4cb63cecb223/GolfScorecard/ee5723a6-256c-4a03-b57e-b542a89553b9.json
new file mode 100644
index 0000000..f1164df
--- /dev/null
+++ b/golf-scorecard-0ed3ac0e-4b0d-452b-b17d-4cb63cecb223/GolfScorecard/ee5723a6-256c-4a03-b57e-b542a89553b9.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"
+ }
+ }
+ }
+}
diff --git a/golf-scorecard-0ed3ac0e-4b0d-452b-b17d-4cb63cecb223/golf-scorecard.gts b/golf-scorecard-0ed3ac0e-4b0d-452b-b17d-4cb63cecb223/golf-scorecard.gts
new file mode 100644
index 0000000..2d73109
--- /dev/null
+++ b/golf-scorecard-0ed3ac0e-4b0d-452b-b17d-4cb63cecb223/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()
+ : '';
+ }
+
+
+
+
+
+
+
+
+
PAR
+ {{#each @model.holes as |hole|}}
+
{{hole.par}}
+ {{/each}}
+
{{this.totalPar}}
+
+
+
SCORE
+ {{#each @model.holes as |hole|}}
+
+ {{hole.strokes}}
+
+ {{/each}}
+
{{@model.totalScore}}
+
+
+
+
+
+
+
+
+
+
+ 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;
+ }
+ }
+
+
+
+
+
+
+
Front Nine
+
+
+
+
+
+
+ {{#if @model.holes}}
+ {{#each (slice @model.holes 0 9) as |hole|}}
+
+ | {{hole.holeNumber}} |
+ {{hole.par}} |
+ {{hole.yards}} |
+ {{hole.strokes}} |
+ {{hole.putts}} |
+
+
+ {{hole.score}}
+
+ |
+
+ {{/each}}
+ {{/if}}
+
+ | OUT |
+ {{this.frontNinePar}} |
+ {{this.frontNineYards}} |
+ {{@model.frontNineTotal}} |
+ {{this.frontNinePutts}} |
+ |
+
+
+
+
+
+
+
+
Back Nine
+
+
+
+
+
+
+ {{#if @model.holes}}
+ {{#each (slice @model.holes 9 18) as |hole|}}
+
+ | {{hole.holeNumber}} |
+ {{hole.par}} |
+ {{hole.yards}} |
+ {{hole.strokes}} |
+ {{hole.putts}} |
+
+
+ {{hole.score}}
+
+ |
+
+ {{/each}}
+ {{/if}}
+
+ | IN |
+ {{this.backNinePar}} |
+ {{this.backNineYards}} |
+ {{@model.backNineTotal}} |
+ {{this.backNinePutts}} |
+ |
+
+
+
+
+
+
+
+
+
+ Total Score
+ {{@model.totalScore}}
+
+
+ To Par
+ {{@model.scoreToPar}}
+
+
+
+
+
+
Player Signature:
+
+ {{#if @model.signedByPlayer}}
+ ✓ Signed
+ {{else}}
+ Awaiting signature
+ {{/if}}
+
+
+
+
Marker Signature:
+
+ {{#if @model.signedByMarker}}
+ ✓ Signed
+ {{else}}
+ Awaiting signature
+ {{/if}}
+
+
+
+
+
+
+
+
+
+
+ };
+
+ 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);
+}