Skip to content

Commit f717126

Browse files
committed
feat(learnEth): Implement priority sorting and default filter visibility
1 parent d4a7f63 commit f717126

File tree

2 files changed

+288
-56
lines changed

2 files changed

+288
-56
lines changed

apps/learneth/src/pages/Home/index.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function HomePage(): JSX.Element {
5757

5858
const [search, setSearch] = useState('')
5959
const debouncedSearch = useDebounced(search, 250)
60-
const [showFilters, setShowFilters] = useState(false)
60+
const [showFilters, setShowFilters] = useState(true)
6161
const [selectedLevels, setSelectedLevels] = useState<number[]>([])
6262
const [selectedTags, setSelectedTags] = useState<string[]>([])
6363

@@ -78,8 +78,8 @@ function HomePage(): JSX.Element {
7878
const rows: Array<{
7979
id: string; levelNum: number; levelText: string; name: string
8080
subtitle: string; preview: string; stepsLen?: number;
81-
duration?: number | string; tags: string[];
82-
}> = []
81+
duration?: number | string; tags: string[]; priority?: number;
82+
}> = []
8383
Object.keys(selectedRepo.group).forEach((levelKey) => {
8484
const levelNum = Number(levelKey) || 1
8585
const levelText = LEVEL_LABEL[levelKey as LevelKey] ?? 'Beginner'
@@ -98,7 +98,8 @@ function HomePage(): JSX.Element {
9898
preview: mdToPlain(entity.description?.content || '', 280),
9999
stepsLen: entity.steps?.length,
100100
duration: entity.metadata?.data?.duration,
101-
tags
101+
tags,
102+
priority: entity.metadata?.data?.priority,
102103
})
103104
})
104105
})
@@ -122,7 +123,27 @@ function HomePage(): JSX.Element {
122123
}
123124
list = list.filter(r => r.tags.some(t => filterTags.has(t)))
124125
}
125-
return list
126+
return list.sort((a, b) => {
127+
if (a.levelNum !== b.levelNum) {
128+
return a.levelNum - b.levelNum
129+
}
130+
131+
const aP = a.priority
132+
const bP = b.priority
133+
const aHasP = aP !== undefined && aP !== null
134+
const bHasP = bP !== undefined && bP !== null
135+
136+
if (aHasP && !bHasP) return -1
137+
if (!aHasP && bHasP) return 1
138+
139+
if (aHasP && bHasP) {
140+
if (aP! !== bP!) {
141+
return aP! - bP!
142+
}
143+
}
144+
145+
return a.name.localeCompare(b.name)
146+
})
126147
}, [flatItems, debouncedSearch, selectedLevels, selectedTags])
127148

128149
return (

apps/learneth/src/redux/models/workshop.ts

Lines changed: 262 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,112 @@ export const repoMap = {
2525
},
2626
}
2727

28+
// This data simulates the API response and includes 'priority'
29+
const mockApiData = {
30+
ids: [
31+
'basics', 'circom-hash-checker', 'er721Auction', 'test-no-priority-Z',
32+
'test-same-priority-A', 'test-no-priority-A', 'advanced-prio-1',
33+
'advanced-no-prio-B', 'advanced-prio-2', 'advanced-no-prio-A'
34+
],
35+
entities: {
36+
// --- LEVEL 1 (Beginner) ---
37+
// Prio: 100
38+
'basics': {
39+
id: 'basics',
40+
level: 1,
41+
name: 'L1 - Basics of Remix (Prio 100)',
42+
description: { content: 'Should be 1st in Level 1.' },
43+
metadata: { data: { id: 'basics', level: 1, name: 'L1 - Basics of Remix (Prio 100)', tags: ['Remix'], priority: 100 } },
44+
steps: []
45+
},
46+
// Prio: 200
47+
'test-same-priority-A': {
48+
id: 'test-same-priority-A',
49+
level: 1,
50+
name: 'L1 - AAA Same Priority (Prio 200)',
51+
description: { content: 'Should be 2nd in Level 1 (Same prio as Hash Checker, but name "AAA" comes first).' },
52+
metadata: { data: { id: 'test-same-priority-A', level: 1, name: 'L1 - AAA Same Priority (Prio 200)', tags: ['Test'], priority: 200 } },
53+
steps: []
54+
},
55+
// Prio: 200
56+
'circom-hash-checker': {
57+
id: 'circom-hash-checker',
58+
level: 1,
59+
name: 'L1 - Hash Checker Tutorial (Prio 200)',
60+
description: { content: 'Should be 3rd in Level 1 (Same prio as "AAA", but "Hash" comes after).' },
61+
metadata: { data: { id: 'circom-hash-checker', level: 1, name: 'L1 - Hash Checker Tutorial (Prio 200)', tags: ['Circom', 'Remix-IDE'], priority: 200 } },
62+
steps: []
63+
},
64+
// No Prio
65+
'test-no-priority-A': {
66+
id: 'test-no-priority-A',
67+
level: 1,
68+
name: 'L1 - Alpha No Priority (No Prio)',
69+
description: { content: 'Should be 4th in Level 1 (Comes after all prioritized items. "Alpha" comes before "Zeta").' },
70+
metadata: { data: { id: 'test-no-priority-A', level: 1, name: 'L1 - Alpha No Priority (No Prio)', tags: ['Test']} }, // no priority
71+
steps: []
72+
},
73+
// No Prio
74+
'test-no-priority-Z': {
75+
id: 'test-no-priority-Z',
76+
level: 1,
77+
name: 'L1 - Zeta No Priority (No Prio)',
78+
description: { content: 'Should be 5th (Last) in Level 1 (Comes after all prioritized items. "Zeta" comes after "Alpha").' },
79+
metadata: { data: { id: 'test-no-priority-Z', level: 1, name: 'L1 - Zeta No Priority (No Prio)', tags: ['Test']} }, // no priority
80+
steps: []
81+
},
82+
83+
// --- LEVEL 2 (Intermediate) ---
84+
// Prio: 100
85+
'er721Auction': {
86+
id: 'er721Auction',
87+
level: 2,
88+
name: 'L2 - NFT Auction Contract (Prio 100)',
89+
description: { content: 'Should be 1st in Level 2.' },
90+
metadata: { data: { id: 'er721Auction', level: 2, name: 'L2 - NFT Auction Contract (Prio 100)', tags: ['Solidity', 'NFT'], priority: 100 } },
91+
steps: []
92+
},
93+
94+
// --- LEVEL 3 (Advanced) ---
95+
// Prio: 100
96+
'advanced-prio-1': {
97+
id: 'advanced-prio-1',
98+
level: 3,
99+
name: 'L3 - Advanced Topic 1 (Prio 100)',
100+
description: { content: 'Should be 1st in Level 3.' },
101+
metadata: { data: { id: 'advanced-prio-1', level: 3, name: 'L3 - Advanced Topic 1 (Prio 100)', tags: ['Advanced'], priority: 100 } },
102+
steps: []
103+
},
104+
// Prio: 300
105+
'advanced-prio-2': {
106+
id: 'advanced-prio-2',
107+
level: 3,
108+
name: 'L3 - Advanced Topic 2 (Prio 300)',
109+
description: { content: 'Should be 2nd in Level 3.' },
110+
metadata: { data: { id: 'advanced-prio-2', level: 3, name: 'L3 - Advanced Topic 2 (Prio 300)', tags: ['Advanced'], priority: 300 } },
111+
steps: []
112+
},
113+
// No Prio
114+
'advanced-no-prio-A': {
115+
id: 'advanced-no-prio-A',
116+
level: 3,
117+
name: 'L3 - Adv Topic A (No Prio)',
118+
description: { content: 'Should be 3rd in Level 3 (After prioritized items. "A" comes before "B").' },
119+
metadata: { data: { id: 'advanced-no-prio-A', level: 3, name: 'L3 - Adv Topic A (No Prio)', tags: ['Advanced']} }, // no priority
120+
steps: []
121+
},
122+
// No Prio
123+
'advanced-no-prio-B': {
124+
id: 'advanced-no-prio-B',
125+
level: 3,
126+
name: 'L3 - Adv Topic B (No Prio)',
127+
description: { content: 'Should be 4th (Last) in Level 3 (After prioritized items. "B" comes after "A").' },
128+
metadata: { data: { id: 'advanced-no-prio-B', level: 3, name: 'L3 - Adv Topic B (No Prio)', tags: ['Advanced']} }, // no priority
129+
steps: []
130+
}
131+
}
132+
}
133+
28134
const Model: ModelType = {
29135
namespace: 'workshop',
30136
state: {
@@ -41,7 +147,7 @@ const Model: ModelType = {
41147
*loadRepo({ payload }, { put, select }) {
42148
yield router.navigate('/home')
43149

44-
toast.info(`loading ${payload.name}/${payload.branch}`)
150+
toast.warn('USING MOCK DATA FOR TESTING', { autoClose: 3000 })
45151

46152
yield put({
47153
type: 'loading/save',
@@ -52,56 +158,8 @@ const Model: ModelType = {
52158

53159
const { list, detail } = yield select((state) => state.workshop)
54160

55-
const url = `${apiUrl}/clone/${encodeURIComponent(payload.name)}/${payload.branch}?${Math.random()}`
56-
57-
let data
58-
try {
59-
const response = yield axios.get(url)
60-
data = response.data
61-
} catch (error) {
62-
console.error('Failed to load workshop:', error)
63-
64-
// Dismiss loading toast and show error
65-
toast.dismiss()
66-
67-
// Extract detailed error message from response
68-
let errorMessage = 'Failed to load workshop'
69-
if (error.response?.data) {
70-
// If the response contains plain text error details (like in the screenshot)
71-
if (typeof error.response.data === 'string') {
72-
errorMessage = error.response.data
73-
}
74-
// If the response has a structured error message
75-
else if (error.response.data.message) {
76-
errorMessage = error.response.data.message
77-
}
78-
// If the response has error details
79-
else if (error.response.data.error) {
80-
errorMessage = error.response.data.error
81-
}
82-
}
83-
// Fallback to axios error message or generic error
84-
else if (error.message) {
85-
errorMessage = error.message
86-
} else {
87-
errorMessage = 'Network error occurred'
88-
}
89-
90-
toast.error(errorMessage)
91-
92-
// Clean up loading state
93-
yield put({
94-
type: 'loading/save',
95-
payload: {
96-
screen: false,
97-
},
98-
})
99-
100-
// Track error event
101-
trackMatomoEvent(remixClient, { category: 'learneth', action: 'load_repo_error', name: `${payload.name}/${payload.branch}`, isClick: false })
102-
103-
return // Exit early on error
104-
}
161+
// Inject mock data
162+
const data = mockApiData
105163

106164
const repoId = `${payload.name}-${payload.branch}`
107165

@@ -191,6 +249,159 @@ const Model: ModelType = {
191249
trackMatomoEvent(remixClient, { category: 'learneth', action: 'load_repo', name: payload.name, isClick: false })
192250
}
193251
},
252+
// *loadRepo({ payload }, { put, select }) {
253+
// yield router.navigate('/home')
254+
255+
// toast.info(`loading ${payload.name}/${payload.branch}`)
256+
257+
// yield put({
258+
// type: 'loading/save',
259+
// payload: {
260+
// screen: true,
261+
// },
262+
// })
263+
264+
// const { list, detail } = yield select((state) => state.workshop)
265+
266+
// const url = `${apiUrl}/clone/${encodeURIComponent(payload.name)}/${payload.branch}?${Math.random()}`
267+
268+
// let data
269+
// try {
270+
// const response = yield axios.get(url)
271+
// data = response.data
272+
// } catch (error) {
273+
// console.error('Failed to load workshop:', error)
274+
275+
// // Dismiss loading toast and show error
276+
// toast.dismiss()
277+
278+
// // Extract detailed error message from response
279+
// let errorMessage = 'Failed to load workshop'
280+
// if (error.response?.data) {
281+
// // If the response contains plain text error details (like in the screenshot)
282+
// if (typeof error.response.data === 'string') {
283+
// errorMessage = error.response.data
284+
// }
285+
// // If the response has a structured error message
286+
// else if (error.response.data.message) {
287+
// errorMessage = error.response.data.message
288+
// }
289+
// // If the response has error details
290+
// else if (error.response.data.error) {
291+
// errorMessage = error.response.data.error
292+
// }
293+
// }
294+
// // Fallback to axios error message or generic error
295+
// else if (error.message) {
296+
// errorMessage = error.message
297+
// } else {
298+
// errorMessage = 'Network error occurred'
299+
// }
300+
301+
// toast.error(errorMessage)
302+
303+
// // Clean up loading state
304+
// yield put({
305+
// type: 'loading/save',
306+
// payload: {
307+
// screen: false,
308+
// },
309+
// })
310+
311+
// // Track error event
312+
// trackMatomoEvent(remixClient, { category: 'learneth', action: 'load_repo_error', name: `${payload.name}/${payload.branch}`, isClick: false })
313+
314+
// return // Exit early on error
315+
// }
316+
317+
// const repoId = `${payload.name}-${payload.branch}`
318+
319+
// for (let i = 0; i < data.ids.length; i++) {
320+
// const {
321+
// steps,
322+
// metadata: {
323+
// data: { steps: metadataSteps },
324+
// },
325+
// } = data.entities[data.ids[i]]
326+
327+
// let newSteps = []
328+
329+
// if (metadataSteps) {
330+
// newSteps = metadataSteps.map((step: any) => {
331+
// return {
332+
// ...steps.find((item: any) => item.name === step.path),
333+
// name: step.name,
334+
// }
335+
// })
336+
// } else {
337+
// newSteps = steps.map((step: any) => ({
338+
// ...step,
339+
// name: step.name.replace('_', ' '),
340+
// }))
341+
// }
342+
343+
// const stepKeysWithFile = ['markdown', 'solidity', 'test', 'answer', 'js', 'vy']
344+
345+
// for (let j = 0; j < newSteps.length; j++) {
346+
// const step = newSteps[j]
347+
// for (let k = 0; k < stepKeysWithFile.length; k++) {
348+
// const key = stepKeysWithFile[k]
349+
// if (step[key]) {
350+
// try {
351+
// step[key].content = null // we load this later
352+
// } catch (error) {
353+
// console.error(error)
354+
// }
355+
// }
356+
// }
357+
// }
358+
// data.entities[data.ids[i]].steps = newSteps
359+
// }
360+
361+
// const workshopState = {
362+
// detail: {
363+
// ...detail,
364+
// [repoId]: {
365+
// ...data,
366+
// group: groupBy(
367+
// data.ids.map((id: string) => pick(data.entities[id], ['level', 'id'])),
368+
// (item: any) => item.level
369+
// ),
370+
// ...payload,
371+
// },
372+
// },
373+
// list: list.map(item => `${item.name}/${item.branch}`).includes(`${payload.name}/${payload.branch}`) ? list : [...list, payload],
374+
// selectedId: repoId,
375+
// }
376+
// yield put({
377+
// type: 'workshop/save',
378+
// payload: workshopState,
379+
// })
380+
381+
// toast.dismiss()
382+
// yield put({
383+
// type: 'loading/save',
384+
// payload: {
385+
// screen: false,
386+
// },
387+
// })
388+
389+
// if (payload.id) {
390+
// const { detail, selectedId } = workshopState
391+
// const { ids, entities } = detail[selectedId]
392+
// for (let i = 0; i < ids.length; i++) {
393+
// const entity = entities[ids[i]]
394+
// if (entity.metadata.data.id === payload.id || i + 1 === payload.id) {
395+
// yield router.navigate(`/list?id=${ids[i]}`)
396+
// break
397+
// }
398+
// }
399+
// }
400+
// // we don't need to track the default repos
401+
// if (payload.name !== 'ethereum/remix-workshops' && payload.name !== 'remix-project-org/remix-workshops') {
402+
// trackMatomoEvent(remixClient, { category: 'learneth', action: 'load_repo', name: payload.name, isClick: false })
403+
// }
404+
// },
194405
*resetAll({ payload }, { put }) {
195406
yield put({
196407
type: 'workshop/save',

0 commit comments

Comments
 (0)