()
| 65 | } |
| 66 | |
| 67 | protected render(): void { |
| 68 | if (this.loading) { |
| 69 | replaceChildren(this.content, |
| 70 | h('div', { className: 'tech-events-loading' }, |
| 71 | h('div', { className: 'loading-spinner' }), |
| 72 | h('span', null, t('components.techEvents.loading')), |
| 73 | ), |
| 74 | ); |
| 75 | return; |
| 76 | } |
| 77 | |
| 78 | if (this.error) { |
| 79 | replaceChildren(this.content, |
| 80 | h('div', { className: 'tech-events-error' }, |
| 81 | h('span', { className: 'error-icon' }, '⚠️'), |
| 82 | h('span', { className: 'error-text' }, this.error), |
| 83 | h('button', { className: 'retry-btn', onClick: () => this.refresh() }, t('common.retry')), |
| 84 | ), |
| 85 | ); |
| 86 | return; |
| 87 | } |
| 88 | |
| 89 | const filteredEvents = this.getFilteredEvents(); |
| 90 | const upcomingConferences = this.events.filter(e => e.type === 'conference' && new Date(e.startDate) >= new Date()); |
| 91 | const mappableCount = upcomingConferences.filter(e => e.coords && !e.coords.virtual).length; |
| 92 | |
| 93 | const tabEntries: [ViewMode, string][] = [ |
| 94 | ['upcoming', t('components.techEvents.upcoming')], |
| 95 | ['conferences', t('components.techEvents.conferences')], |
| 96 | ['earnings', t('components.techEvents.earnings')], |
| 97 | ['all', t('components.techEvents.all')], |
| 98 | ]; |
| 99 | |
| 100 | replaceChildren(this.content, |
| 101 | h('div', { className: 'tech-events-panel' }, |
| 102 | h('div', { className: 'tech-events-tabs' }, |
| 103 | ...tabEntries.map(([view, label]) => |
| 104 | h('button', { |
| 105 | className: `tab ${this.viewMode === view ? 'active' : ''}`, |
| 106 | dataset: { view }, |
| 107 | onClick: () => { this.viewMode = view; this.render(); }, |
| 108 | }, label), |
| 109 | ), |
| 110 | ), |
| 111 | h('div', { className: 'tech-events-stats' }, |
| 112 | h('span', { className: 'stat' }, `📅 ${t('components.techEvents.conferencesCount', { count: String(upcomingConferences.length) })}`), |
| 113 | h('span', { className: 'stat' }, `📍 ${t('components.techEvents.onMap', { count: String(mappableCount) })}`), |
| 114 | h('a', { href: 'https://www.techmeme.com/events', target: '_blank', rel: 'noopener', className: 'source-link' }, t('components.techEvents.techmemeEvents')), |
| 115 | ), |
| 116 | h('div', { className: 'tech-events-list' }, |
| 117 | ...(filteredEvents.length > 0 |
| 118 | ? filteredEvents.map(e => this.buildEvent(e)) |
| 119 | : [h('div', { className: 'empty-state' }, t('components.techEvents.noEvents'))]), |
| 120 | ), |
| 121 | ), |
| 122 | ); |
| 123 | } |
| 124 |
no test coverage detected