ace-interview-prep 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/commands/add.js +92 -0
  4. package/dist/commands/feedback.js +133 -0
  5. package/dist/commands/generate.js +224 -0
  6. package/dist/commands/init.js +100 -0
  7. package/dist/commands/list.js +107 -0
  8. package/dist/commands/reset.js +68 -0
  9. package/dist/commands/score.js +70 -0
  10. package/dist/commands/setup.js +84 -0
  11. package/dist/commands/test.js +85 -0
  12. package/dist/index.js +72 -0
  13. package/dist/lib/categories.js +103 -0
  14. package/dist/lib/config.js +61 -0
  15. package/dist/lib/llm.js +134 -0
  16. package/dist/lib/paths.js +38 -0
  17. package/dist/lib/scaffold.js +110 -0
  18. package/dist/lib/scorecard.js +116 -0
  19. package/dist/prompts/code-review.md +59 -0
  20. package/dist/prompts/design-review.md +67 -0
  21. package/dist/prompts/question-brainstorm.md +31 -0
  22. package/dist/prompts/question-generate.md +65 -0
  23. package/dist/templates/design/notes.md.hbs +27 -0
  24. package/dist/templates/js-ts/solution.test.ts.hbs +11 -0
  25. package/dist/templates/js-ts/solution.ts.hbs +11 -0
  26. package/dist/templates/leetcode-algo/solution.test.ts.hbs +11 -0
  27. package/dist/templates/leetcode-algo/solution.ts.hbs +11 -0
  28. package/dist/templates/leetcode-ds/solution.test.ts.hbs +11 -0
  29. package/dist/templates/leetcode-ds/solution.ts.hbs +11 -0
  30. package/dist/templates/react-apps/App.test.tsx.hbs +16 -0
  31. package/dist/templates/react-apps/App.tsx.hbs +16 -0
  32. package/dist/templates/readme.md.hbs +9 -0
  33. package/dist/templates/web-components/component.test.ts.hbs +11 -0
  34. package/dist/templates/web-components/component.ts.hbs +22 -0
  35. package/dist/templates/web-components/index.html.hbs +12 -0
  36. package/package.json +72 -0
  37. package/questions/design-be/url-shortener/README.md +23 -0
  38. package/questions/design-be/url-shortener/notes.md +27 -0
  39. package/questions/design-be/url-shortener/scorecard.json +1 -0
  40. package/questions/design-fe/news-feed/README.md +22 -0
  41. package/questions/design-fe/news-feed/notes.md +27 -0
  42. package/questions/design-fe/news-feed/scorecard.json +1 -0
  43. package/questions/design-full/google-docs/README.md +22 -0
  44. package/questions/design-full/google-docs/notes.md +27 -0
  45. package/questions/design-full/google-docs/scorecard.json +1 -0
  46. package/questions/js-ts/debounce/README.md +86 -0
  47. package/questions/js-ts/debounce/scorecard.json +9 -0
  48. package/questions/js-ts/debounce/solution.test.ts +128 -0
  49. package/questions/js-ts/debounce/solution.ts +4 -0
  50. package/questions/leetcode-algo/two-sum/README.md +58 -0
  51. package/questions/leetcode-algo/two-sum/scorecard.json +1 -0
  52. package/questions/leetcode-algo/two-sum/solution.test.ts +55 -0
  53. package/questions/leetcode-algo/two-sum/solution.ts +4 -0
  54. package/questions/leetcode-ds/lru-cache/README.md +70 -0
  55. package/questions/leetcode-ds/lru-cache/scorecard.json +1 -0
  56. package/questions/leetcode-ds/lru-cache/solution.test.ts +82 -0
  57. package/questions/leetcode-ds/lru-cache/solution.ts +14 -0
  58. package/questions/react-apps/todo-app/App.test.tsx +130 -0
  59. package/questions/react-apps/todo-app/App.tsx +10 -0
  60. package/questions/react-apps/todo-app/README.md +23 -0
  61. package/questions/react-apps/todo-app/scorecard.json +9 -0
  62. package/questions/web-components/star-rating/README.md +45 -0
  63. package/questions/web-components/star-rating/component.test.ts +64 -0
  64. package/questions/web-components/star-rating/component.ts +28 -0
  65. package/questions/web-components/star-rating/index.html +14 -0
  66. package/questions/web-components/star-rating/scorecard.json +9 -0
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { twoSum } from './solution';
3
+
4
+ describe('twoSum', () => {
5
+ it('basic case: [2,7,11,15] target 9 returns [0,1]', () => {
6
+ const nums = [2, 7, 11, 15];
7
+ const result = twoSum(nums, 9);
8
+ expect(result).toHaveLength(2);
9
+ expect(result.sort()).toEqual([0, 1]);
10
+ expect(nums[result[0]] + nums[result[1]]).toBe(9);
11
+ });
12
+
13
+ it('handles negative numbers', () => {
14
+ const result = twoSum([-1, -2, -3, -4, -5], -8);
15
+ expect(result).toHaveLength(2);
16
+ const [i, j] = result;
17
+ expect([-1, -2, -3, -4, -5][i] + [-1, -2, -3, -4, -5][j]).toBe(-8);
18
+ });
19
+
20
+ it('handles zero in array', () => {
21
+ const result = twoSum([0, 4, 3, 0], 0);
22
+ expect(result).toHaveLength(2);
23
+ const [i, j] = result;
24
+ expect(i).not.toBe(j);
25
+ expect([0, 4, 3, 0][i] + [0, 4, 3, 0][j]).toBe(0);
26
+ });
27
+
28
+ it('handles large array', () => {
29
+ const nums = Array.from({ length: 10000 }, (_, i) => i);
30
+ const target = 9998;
31
+ const result = twoSum(nums, target);
32
+ expect(result).toHaveLength(2);
33
+ expect(nums[result[0]] + nums[result[1]]).toBe(target);
34
+ });
35
+
36
+ it('handles duplicate values', () => {
37
+ const result = twoSum([3, 3], 6);
38
+ expect(result).toHaveLength(2);
39
+ expect(result).toContain(0);
40
+ expect(result).toContain(1);
41
+ expect(result[0]).not.toBe(result[1]);
42
+ });
43
+
44
+ it('adjacent elements sum to target', () => {
45
+ const result = twoSum([1, 2, 3, 4], 7);
46
+ expect(result).toHaveLength(2);
47
+ expect([1, 2, 3, 4][result[0]] + [1, 2, 3, 4][result[1]]).toBe(7);
48
+ });
49
+
50
+ it('single pair in longer array', () => {
51
+ const result = twoSum([1, 2, 3, 4, 5, 6, 7, 8, 9], 17);
52
+ expect(result).toHaveLength(2);
53
+ expect([1, 2, 3, 4, 5, 6, 7, 8, 9][result[0]] + [1, 2, 3, 4, 5, 6, 7, 8, 9][result[1]]).toBe(17);
54
+ });
55
+ });
@@ -0,0 +1,4 @@
1
+ export function twoSum(nums: number[], target: number): [number, number] {
2
+ // TODO: implement
3
+ return [0, 0];
4
+ }
@@ -0,0 +1,70 @@
1
+ # Implement an LRU Cache
2
+
3
+ **Category:** LeetCode Data Structures
4
+ **Difficulty:** Hard
5
+ **Suggested Time:** ~45 minutes
6
+
7
+ ---
8
+
9
+ ## Problem
10
+
11
+ Implement an LRU (Least Recently Used) Cache class with `get(key)` and `put(key, value)` methods. Both operations must run in **O(1)** time complexity.
12
+
13
+ The constructor takes a `capacity` parameter that determines the maximum number of key-value pairs the cache can hold. When the cache reaches capacity and a new item is inserted, the least recently used item must be evicted.
14
+
15
+ ## Class Signature
16
+
17
+ ```ts
18
+ class LRUCache {
19
+ constructor(capacity: number)
20
+ get(key: number): number
21
+ put(key: number, value: number): void
22
+ }
23
+ ```
24
+
25
+ - **`constructor(capacity)`** — Initialize the cache with a maximum capacity.
26
+ - **`get(key)`** — Return the value for the key if it exists, otherwise return `-1`. Accessing a key updates its recency (makes it "most recently used").
27
+ - **`put(key, value)`** — Insert or update the value. If the key already exists, update its value and recency. If the cache is at capacity, evict the least recently used item before inserting.
28
+
29
+ ## Examples
30
+
31
+ ### Example 1
32
+
33
+ ```ts
34
+ const cache = new LRUCache(2);
35
+
36
+ cache.put(1, 1);
37
+ cache.put(2, 2);
38
+ cache.get(1); // returns 1
39
+ cache.put(3, 3); // evicts key 2
40
+ cache.get(2); // returns -1 (not found)
41
+ cache.put(4, 4); // evicts key 1
42
+ cache.get(1); // returns -1 (not found)
43
+ cache.get(3); // returns 3
44
+ cache.get(4); // returns 4
45
+ ```
46
+
47
+ ### Example 2
48
+
49
+ ```ts
50
+ const cache = new LRUCache(1);
51
+
52
+ cache.put(1, 1);
53
+ cache.get(1); // returns 1
54
+ cache.put(2, 2); // evicts key 1
55
+ cache.get(1); // returns -1
56
+ cache.get(2); // returns 2
57
+ ```
58
+
59
+ ## Constraints
60
+
61
+ - `1 <= capacity <= 3000`
62
+ - `0 <= key <= 10^4`
63
+ - `0 <= value <= 10^5`
64
+ - At most `2 * 10^5` calls will be made to `get` and `put`.
65
+
66
+ ## Hints
67
+
68
+ - A hash map gives O(1) lookup, but doesn't track order.
69
+ - A doubly linked list gives O(1) add/remove at head or tail.
70
+ - Combine both: map for O(1) lookup, doubly linked list for O(1) recency updates.
@@ -0,0 +1 @@
1
+ {"title":"Implement an LRU Cache","category":"leetcode-ds","difficulty":"hard","suggestedTime":45,"status":"untouched","attempts":[],"llmFeedback":null}
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { LRUCache } from './solution';
3
+
4
+ describe('LRUCache', () => {
5
+ it('basic get and put', () => {
6
+ const cache = new LRUCache(2);
7
+ cache.put(1, 1);
8
+ cache.put(2, 2);
9
+ expect(cache.get(1)).toBe(1);
10
+ expect(cache.get(2)).toBe(2);
11
+ });
12
+
13
+ it('evicts least recently used when full', () => {
14
+ const cache = new LRUCache(2);
15
+ cache.put(1, 1);
16
+ cache.put(2, 2);
17
+ cache.put(3, 3); // evicts key 1
18
+ expect(cache.get(1)).toBe(-1);
19
+ expect(cache.get(2)).toBe(2);
20
+ expect(cache.get(3)).toBe(3);
21
+ });
22
+
23
+ it('get updates recency (accessed key is not evicted next)', () => {
24
+ const cache = new LRUCache(2);
25
+ cache.put(1, 1);
26
+ cache.put(2, 2);
27
+ cache.get(1); // makes 1 most recently used
28
+ cache.put(3, 3); // evicts key 2, not 1
29
+ expect(cache.get(1)).toBe(1);
30
+ expect(cache.get(2)).toBe(-1);
31
+ expect(cache.get(3)).toBe(3);
32
+ });
33
+
34
+ it('put existing key updates value and recency', () => {
35
+ const cache = new LRUCache(2);
36
+ cache.put(1, 1);
37
+ cache.put(2, 2);
38
+ cache.put(1, 10); // update value, 1 becomes most recently used
39
+ expect(cache.get(1)).toBe(10);
40
+ cache.put(3, 3); // evicts key 2
41
+ expect(cache.get(1)).toBe(10);
42
+ expect(cache.get(2)).toBe(-1);
43
+ });
44
+
45
+ it('capacity of 1', () => {
46
+ const cache = new LRUCache(1);
47
+ cache.put(1, 1);
48
+ expect(cache.get(1)).toBe(1);
49
+ cache.put(2, 2);
50
+ expect(cache.get(1)).toBe(-1);
51
+ expect(cache.get(2)).toBe(2);
52
+ });
53
+
54
+ it('accessing a key prevents its eviction', () => {
55
+ const cache = new LRUCache(2);
56
+ cache.put(1, 1);
57
+ cache.put(2, 2);
58
+ cache.get(2); // 2 is now most recently used
59
+ cache.put(3, 3); // evicts 1
60
+ expect(cache.get(1)).toBe(-1);
61
+ expect(cache.get(2)).toBe(2);
62
+ expect(cache.get(3)).toBe(3);
63
+ });
64
+
65
+ it('multiple evictions in sequence', () => {
66
+ const cache = new LRUCache(2);
67
+ cache.put(1, 1);
68
+ cache.put(2, 2);
69
+ cache.put(3, 3); // evicts 1
70
+ cache.put(4, 4); // evicts 2
71
+ expect(cache.get(1)).toBe(-1);
72
+ expect(cache.get(2)).toBe(-1);
73
+ expect(cache.get(3)).toBe(3);
74
+ expect(cache.get(4)).toBe(4);
75
+ });
76
+
77
+ it('empty cache returns -1 for get', () => {
78
+ const cache = new LRUCache(2);
79
+ expect(cache.get(1)).toBe(-1);
80
+ expect(cache.get(99)).toBe(-1);
81
+ });
82
+ });
@@ -0,0 +1,14 @@
1
+ export class LRUCache {
2
+ constructor(private capacity: number) {
3
+ // TODO: implement
4
+ }
5
+
6
+ get(key: number): number {
7
+ // TODO: implement
8
+ return -1;
9
+ }
10
+
11
+ put(key: number, value: number): void {
12
+ // TODO: implement
13
+ }
14
+ }
@@ -0,0 +1,130 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import App from './App';
4
+
5
+ function getInput() {
6
+ return (
7
+ screen.queryByPlaceholderText(/add|what needs|new todo|todo/i) ??
8
+ screen.getByRole('textbox')
9
+ );
10
+ }
11
+
12
+ function getAddButton() {
13
+ return screen.getByRole('button', { name: /add|submit/i });
14
+ }
15
+
16
+ describe('Todo App', () => {
17
+ it('renders heading', () => {
18
+ render(<App />);
19
+ expect(screen.getByRole('heading', { name: /todo app/i })).toBeInTheDocument();
20
+ });
21
+
22
+ it('can add a todo', () => {
23
+ render(<App />);
24
+ const input = getInput();
25
+ const addButton = getAddButton();
26
+ fireEvent.change(input, { target: { value: 'Buy milk' } });
27
+ fireEvent.click(addButton);
28
+ expect(screen.getByText('Buy milk')).toBeInTheDocument();
29
+ });
30
+
31
+ it('displays added todo', () => {
32
+ render(<App />);
33
+ const input = getInput();
34
+ const addButton = getAddButton();
35
+ fireEvent.change(input, { target: { value: 'Walk the dog' } });
36
+ fireEvent.click(addButton);
37
+ expect(screen.getByText('Walk the dog')).toBeInTheDocument();
38
+ });
39
+
40
+ it('can toggle todo complete', () => {
41
+ render(<App />);
42
+ const input = getInput();
43
+ const addButton = getAddButton();
44
+ fireEvent.change(input, { target: { value: 'Test todo' } });
45
+ fireEvent.click(addButton);
46
+ const checkbox =
47
+ screen.getByText('Test todo').closest('li')?.querySelector('input[type="checkbox"]') ??
48
+ screen.getByRole('checkbox');
49
+ fireEvent.click(checkbox);
50
+ expect(checkbox).toBeChecked();
51
+ });
52
+
53
+ it('can delete a todo', () => {
54
+ render(<App />);
55
+ const input = getInput();
56
+ const addButton = getAddButton();
57
+ fireEvent.change(input, { target: { value: 'Delete me' } });
58
+ fireEvent.click(addButton);
59
+ expect(screen.getByText('Delete me')).toBeInTheDocument();
60
+ const deleteButton =
61
+ screen.queryByRole('button', { name: /delete|remove|x/i }) ??
62
+ screen.queryByLabelText(/delete|remove/i);
63
+ if (!deleteButton) throw new Error('Delete button not found');
64
+ fireEvent.click(deleteButton);
65
+ expect(screen.queryByText('Delete me')).not.toBeInTheDocument();
66
+ });
67
+
68
+ it('filter shows only active todos', () => {
69
+ render(<App />);
70
+ const input = getInput();
71
+ const addButton = getAddButton();
72
+ fireEvent.change(input, { target: { value: 'Active todo' } });
73
+ fireEvent.click(addButton);
74
+ fireEvent.change(input, { target: { value: 'Completed todo' } });
75
+ fireEvent.click(addButton);
76
+ const checkboxes = screen.getAllByRole('checkbox');
77
+ fireEvent.click(checkboxes[1]);
78
+ const activeFilter =
79
+ screen.queryByRole('button', { name: /active/i }) ?? screen.queryByText(/active/i);
80
+ if (!activeFilter) throw new Error('Active filter not found');
81
+ fireEvent.click(activeFilter);
82
+ expect(screen.getByText('Active todo')).toBeInTheDocument();
83
+ expect(screen.queryByText('Completed todo')).not.toBeInTheDocument();
84
+ });
85
+
86
+ it('filter shows only completed todos', () => {
87
+ render(<App />);
88
+ const input = getInput();
89
+ const addButton = getAddButton();
90
+ fireEvent.change(input, { target: { value: 'Todo one' } });
91
+ fireEvent.click(addButton);
92
+ fireEvent.change(input, { target: { value: 'Todo two' } });
93
+ fireEvent.click(addButton);
94
+ const checkboxes = screen.getAllByRole('checkbox');
95
+ fireEvent.click(checkboxes[0]);
96
+ const completedFilter =
97
+ screen.queryByRole('button', { name: /completed/i }) ?? screen.queryByText(/completed/i);
98
+ if (!completedFilter) throw new Error('Completed filter not found');
99
+ fireEvent.click(completedFilter);
100
+ expect(screen.getByText('Todo one')).toBeInTheDocument();
101
+ expect(screen.queryByText('Todo two')).not.toBeInTheDocument();
102
+ });
103
+
104
+ it('shows remaining count', () => {
105
+ render(<App />);
106
+ const input = getInput();
107
+ const addButton = getAddButton();
108
+ fireEvent.change(input, { target: { value: 'First' } });
109
+ fireEvent.click(addButton);
110
+ fireEvent.change(input, { target: { value: 'Second' } });
111
+ fireEvent.click(addButton);
112
+ const countEl =
113
+ screen.queryByText(/2.*(item|remaining|left)/i) ?? screen.queryByText(/^2$/);
114
+ expect(countEl).toBeInTheDocument();
115
+ });
116
+
117
+ it('can add multiple todos', () => {
118
+ render(<App />);
119
+ const input = getInput();
120
+ const addButton = getAddButton();
121
+ const todos = ['First todo', 'Second todo', 'Third todo'];
122
+ todos.forEach((todo) => {
123
+ fireEvent.change(input, { target: { value: todo } });
124
+ fireEvent.click(addButton);
125
+ });
126
+ todos.forEach((todo) => {
127
+ expect(screen.getByText(todo)).toBeInTheDocument();
128
+ });
129
+ });
130
+ });
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+
3
+ export default function App() {
4
+ return (
5
+ <div>
6
+ <h1>Todo App</h1>
7
+ {/* TODO: implement */}
8
+ </div>
9
+ );
10
+ }
@@ -0,0 +1,23 @@
1
+ # Build a Todo App with Filters
2
+
3
+ ## Problem
4
+
5
+ Build a React Todo app with the following features:
6
+
7
+ - Add todos
8
+ - Toggle complete
9
+ - Delete todos
10
+ - Filter by All / Active / Completed
11
+ - Show count of remaining items
12
+
13
+ ## Category
14
+
15
+ React Web Apps
16
+
17
+ ## Difficulty
18
+
19
+ Medium
20
+
21
+ ## Suggested Time
22
+
23
+ ~45 minutes
@@ -0,0 +1,9 @@
1
+ {
2
+ "title": "Build a Todo App with Filters",
3
+ "category": "react-apps",
4
+ "difficulty": "medium",
5
+ "suggestedTime": 45,
6
+ "status": "untouched",
7
+ "attempts": [],
8
+ "llmFeedback": null
9
+ }
@@ -0,0 +1,45 @@
1
+ # Build a Star Rating Web Component
2
+
3
+ **Category:** Web Components
4
+ **Difficulty:** Medium
5
+ **Suggested Time:** ~35 minutes
6
+
7
+ ---
8
+
9
+ ## Problem
10
+
11
+ Build a `<star-rating>` custom element that displays 5 stars, allows users to click to rate, has a `value` attribute/property, and dispatches a `change` event when the rating changes.
12
+
13
+ ## Requirements
14
+
15
+ - **Display** — Render 5 star elements (you may use Unicode stars ★/☆, SVG, or styled spans).
16
+ - **Click to rate** — Clicking a star sets the rating to that star's index (1–5).
17
+ - **`value` attribute** — The component accepts a `value` attribute (e.g. `<star-rating value="3">`) to show the initial or current rating.
18
+ - **`value` property** — The component exposes a `value` getter/setter that reflects and updates the rating.
19
+ - **`change` event** — When the user clicks a star, dispatch a `change` event with the new value (e.g. `detail: { value: 3 }`).
20
+
21
+ ## Example Usage
22
+
23
+ ```html
24
+ <star-rating value="3"></star-rating>
25
+ ```
26
+
27
+ ```js
28
+ const el = document.querySelector('star-rating');
29
+ el.value = 4;
30
+ el.addEventListener('change', (e) => console.log('New rating:', e.detail.value));
31
+ ```
32
+
33
+ ## Constraints
34
+
35
+ - Use the Custom Elements API (extend `HTMLElement`).
36
+ - Use Shadow DOM for encapsulation.
37
+ - Observe the `value` attribute and sync it with the internal state.
38
+ - Clamp `value` to 0–5 (0 = no stars selected).
39
+
40
+ ## Hints
41
+
42
+ - Use `attachShadow({ mode: 'open' })` in the constructor.
43
+ - Use `static get observedAttributes()` to return `['value']`.
44
+ - In `attributeChangedCallback`, parse the attribute and update the display.
45
+ - Use `CustomEvent` with `detail: { value }` for the change event.
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import './component';
3
+
4
+ describe('star-rating', () => {
5
+ let el: HTMLElement & { value: number };
6
+
7
+ beforeEach(() => {
8
+ el = document.createElement('star-rating') as HTMLElement & { value: number };
9
+ document.body.appendChild(el);
10
+ });
11
+
12
+ afterEach(() => {
13
+ el.remove();
14
+ });
15
+
16
+ it('renders 5 star elements', () => {
17
+ const stars = el.shadowRoot?.querySelectorAll('[data-star]') ?? [];
18
+ expect(stars.length).toBe(5);
19
+ });
20
+
21
+ it('default value is 0', () => {
22
+ expect(el.value).toBe(0);
23
+ });
24
+
25
+ it('setting value attribute updates display', () => {
26
+ el.setAttribute('value', '3');
27
+ expect(el.value).toBe(3);
28
+ });
29
+
30
+ it('clicking a star updates value', () => {
31
+ const stars = el.shadowRoot?.querySelectorAll('[data-star]') ?? [];
32
+ (stars[2] as HTMLElement).click();
33
+ expect(el.value).toBe(3);
34
+ });
35
+
36
+ it('dispatches change event on click', () => {
37
+ let receivedValue: number | undefined;
38
+ const handler = (e: Event) => {
39
+ receivedValue = (e as CustomEvent).detail?.value;
40
+ };
41
+ el.addEventListener('change', handler);
42
+
43
+ const stars = el.shadowRoot?.querySelectorAll('[data-star]') ?? [];
44
+ (stars[3] as HTMLElement).click();
45
+
46
+ expect(receivedValue).toBe(4);
47
+ el.removeEventListener('change', handler);
48
+ });
49
+
50
+ it('value property reflects attribute', () => {
51
+ el.setAttribute('value', '2');
52
+ expect(el.value).toBe(2);
53
+ });
54
+
55
+ it('setting value property updates attribute', () => {
56
+ el.value = 5;
57
+ expect(el.getAttribute('value')).toBe('5');
58
+ });
59
+
60
+ it('clamps value to 0-5', () => {
61
+ el.setAttribute('value', '10');
62
+ expect(el.value).toBeLessThanOrEqual(5);
63
+ });
64
+ });
@@ -0,0 +1,28 @@
1
+ export class StarRating extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ this.attachShadow({ mode: 'open' });
5
+ }
6
+
7
+ connectedCallback() {
8
+ // TODO: implement - render 5 stars, handle click, support value attribute
9
+ }
10
+
11
+ static get observedAttributes() {
12
+ return ['value'];
13
+ }
14
+
15
+ attributeChangedCallback(_name: string, _oldValue: string | null, _newValue: string | null) {
16
+ // TODO: implement
17
+ }
18
+
19
+ get value(): number {
20
+ return 0; // TODO: implement
21
+ }
22
+
23
+ set value(_val: number) {
24
+ // TODO: implement
25
+ }
26
+ }
27
+
28
+ customElements.define('star-rating', StarRating);
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Star Rating Component</title>
7
+ </head>
8
+ <body>
9
+ <h1>Star Rating</h1>
10
+ <star-rating value="3"></star-rating>
11
+
12
+ <script type="module" src="./component.ts"></script>
13
+ </body>
14
+ </html>
@@ -0,0 +1,9 @@
1
+ {
2
+ "title": "Build a Star Rating Component",
3
+ "category": "web-components",
4
+ "difficulty": "medium",
5
+ "suggestedTime": 35,
6
+ "status": "untouched",
7
+ "attempts": [],
8
+ "llmFeedback": null
9
+ }