Skip to content

Commit efa46a0

Browse files
committed
Refactor Footer component to be a web component
1 parent 49fd790 commit efa46a0

9 files changed

Lines changed: 254 additions & 257 deletions

File tree

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,16 @@
1-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2-
import { Footer } from '@components/Footer/client'
1+
// @vitest-environment happy-dom
2+
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest'
3+
import { FooterElement } from '@components/Footer/client'
34

45
// Mock the selectors module
5-
vi.mock('@components/Footer/selectors')
6+
vi.mock('@components/Footer/client/selectors', () => ({
7+
getHireMeAnchorElement: vi.fn(),
8+
}))
69

710
import { getHireMeAnchorElement } from '@components/Footer/client/selectors'
811

9-
// No MockDate needed, using fake timers for Date
10-
11-
describe('Footer', () => {
12-
const mockAnchor = {
13-
innerHTML: '',
14-
style: {
15-
display: '',
16-
},
17-
}
18-
12+
describe('FooterElement', () => {
1913
beforeEach(() => {
20-
;(getHireMeAnchorElement as any).mockReturnValue(mockAnchor)
2114
vi.useFakeTimers()
2215
vi.setSystemTime(new Date('2024-10-17'))
2316
})
@@ -27,30 +20,18 @@ describe('Footer', () => {
2720
vi.clearAllMocks()
2821
})
2922

30-
it('should update hire me anchor with current date on init', () => {
23+
it('updates the hire me anchor with availability copy when connected', () => {
3124
const anchor = {
32-
...mockAnchor,
33-
innerHTML: 'Old text',
34-
style: { ...mockAnchor.style, display: 'none' },
35-
}
36-
37-
;(getHireMeAnchorElement as any).mockReturnValue(anchor)
25+
innerHTML: '',
26+
style: { display: 'none' },
27+
} as unknown as HTMLAnchorElement
28+
;(getHireMeAnchorElement as Mock).mockReturnValue(anchor)
3829

39-
Footer.init()
30+
const element = new FooterElement()
31+
element.connectedCallback()
4032

33+
expect(getHireMeAnchorElement).toHaveBeenCalledWith(element)
4134
expect(anchor.innerHTML).toBe('Available September, 2024. Hire Me Now')
4235
expect(anchor.style.display).toBe('inline-block')
4336
})
44-
45-
it('should handle pause', () => {
46-
expect(() => Footer.pause()).not.toThrow()
47-
})
48-
49-
it('should handle resume', () => {
50-
expect(() => Footer.resume()).not.toThrow()
51-
})
52-
53-
it('should handle reset', () => {
54-
expect(() => Footer.reset()).not.toThrow()
55-
})
5637
})
Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,61 @@
11
/**
2-
* Script for the Footer component
2+
* Footer web component
3+
* Updates the "Hire Me" CTA text once connected
34
*/
5+
import { LitElement } from 'lit'
46
import { getHireMeAnchorElement } from '@components/Footer/client/selectors'
57
import { addScriptBreadcrumb } from '@components/scripts/errors'
68
import { handleScriptError } from '@components/scripts/errors/handler'
79

8-
class Footer {
9-
static scriptName = 'Footer'
10+
class FooterElement extends LitElement {
11+
private hireMeAnchor: HTMLAnchorElement | null = null
1012

11-
private static getMonthName(date: Date): string {
12-
const monthDate = new Date(date)
13-
monthDate.setMonth(monthDate.getMonth() - 1)
14-
return monthDate.toLocaleString('en-US', { month: 'long' })
13+
override createRenderRoot() {
14+
return this
1515
}
1616

17-
static init(): void {
18-
const context = { scriptName: Footer.scriptName, operation: 'init' }
17+
override connectedCallback(): void {
18+
super.connectedCallback()
19+
this.initialize()
20+
}
21+
22+
private initialize(): void {
23+
const context = { scriptName: 'FooterElement', operation: 'initialize' }
1924
addScriptBreadcrumb(context)
2025

2126
try {
22-
const anchor = getHireMeAnchorElement()
23-
const date = new Date()
24-
const month = Footer.getMonthName(date)
25-
const year = date.getFullYear()
26-
anchor.innerHTML = `Available ${month}, ${year}. Hire Me Now`
27-
anchor.style.display = 'inline-block'
27+
if (!this.hireMeAnchor) {
28+
this.hireMeAnchor = getHireMeAnchorElement(this)
29+
}
30+
31+
this.updateHireMeCopy()
2832
} catch (error) {
29-
// Footer date is optional enhancement
3033
handleScriptError(error, context)
3134
}
3235
}
3336

34-
static pause(): void {
35-
// No pause functionality needed for Footer
36-
}
37+
private updateHireMeCopy(): void {
38+
if (!this.hireMeAnchor) {
39+
return
40+
}
41+
42+
const date = new Date()
43+
const month = FooterElement.getMonthName(date)
44+
const year = date.getFullYear()
3745

38-
static resume(): void {
39-
// No resume functionality needed for Footer
46+
this.hireMeAnchor.innerHTML = `Available ${month}, ${year}. Hire Me Now`
47+
this.hireMeAnchor.style.display = 'inline-block'
4048
}
4149

42-
static reset(): void {
43-
// No reset functionality needed for Footer
50+
private static getMonthName(date: Date): string {
51+
const monthDate = new Date(date)
52+
monthDate.setMonth(monthDate.getMonth() - 1)
53+
return monthDate.toLocaleString('en-US', { month: 'long' })
4454
}
4555
}
4656

47-
export { Footer }
57+
if (!customElements.get('site-footer')) {
58+
customElements.define('site-footer', FooterElement)
59+
}
60+
61+
export { FooterElement }

src/components/Footer/client/selectors.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ export const SELECTORS = {
1212
/**
1313
* Getter for the header <span> HTML element that wraps the menu
1414
*/
15-
export const getHireMeAnchorElement = (): HTMLAnchorElement => {
16-
const anchor = document.querySelector(SELECTORS.hireMeAnchor)
15+
export const getHireMeAnchorElement = (scope: ParentNode = document): HTMLAnchorElement => {
16+
const anchor = scope.querySelector(SELECTORS.hireMeAnchor)
1717
if (!isAnchorElement(anchor)) {
1818
throw new ClientScriptError({
19-
message: `Footer anchor for "Hire Me" element, selector: ${SELECTORS.hireMeAnchor}`
19+
message: `Footer anchor for "Hire Me" element, selector: ${SELECTORS.hireMeAnchor}`,
2020
})
2121
}
2222
return anchor

src/components/Footer/footer.css

Lines changed: 0 additions & 58 deletions
This file was deleted.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
@keyframes arrowBounce {
2+
100% {
3+
transform: translate(1.5em);
4+
}
5+
}
6+
7+
@media (width >= 640px) {
8+
.footerGrid {
9+
display: grid;
10+
gap: 0.5rem 1rem;
11+
grid-template-areas:
12+
"contact social"
13+
"contact channels"
14+
"bottom-row bottom-row"
15+
"copyright copyright";
16+
grid-template-columns: 1fr 1fr;
17+
}
18+
19+
.footerContact {
20+
grid-area: contact;
21+
}
22+
23+
.footerSocial {
24+
grid-area: social;
25+
}
26+
27+
.footerBottomRow {
28+
grid-area: bottom-row;
29+
}
30+
31+
.footerCopyright {
32+
grid-area: copyright;
33+
}
34+
}
35+
36+
@media (width >= 1024px) {
37+
.footerGrid {
38+
grid-template:
39+
"hire-me social bottom-row" 2rem
40+
"contact social bottom-row" auto
41+
"copyright copyright copyright" auto
42+
/ 2fr 1fr 1fr;
43+
}
44+
45+
.footerHireMe {
46+
align-self: end;
47+
grid-area: hire-me;
48+
margin-top: 2rem;
49+
}
50+
51+
.footerHireMeAnchor {
52+
margin-top: 0.5rem;
53+
}
54+
}
55+
56+
.footerHireMeAnchor:hover::after,
57+
.footerHireMeAnchor:focus::after {
58+
animation: arrowBounce 1s alternate;
59+
}
60+
61+
.footerFeedIcon:hover :global(.icon),
62+
.footerFeedIcon:focus :global(.icon) {
63+
height: 3em;
64+
width: 3em;
65+
}

0 commit comments

Comments
 (0)