@conorheffron/ironoc-frontend 9.1.5 → 9.1.6

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conorheffron/ironoc-frontend",
3
- "version": "9.1.5",
3
+ "version": "9.1.6",
4
4
  "private": false,
5
5
  "license": "GPL-3.0-or-later",
6
6
  "dependencies": {
package/src/App.test.js CHANGED
@@ -9,6 +9,11 @@ import AppNavBar from './AppNavbar';
9
9
  import About from './components/About';
10
10
  import Footer from './Footer';
11
11
  import { Router, useLocation, MemoryRouter } from 'react-router';
12
+ import { trackClickOut } from './utils/activityTracker';
13
+
14
+ jest.mock('./utils/activityTracker', () => ({
15
+ trackClickOut: jest.fn(),
16
+ }));
12
17
 
13
18
  describe('AppNavBar', () => {
14
19
  test('renders AppNavBar component correctly', () => {
@@ -79,6 +84,13 @@ describe('AppNavBar', () => {
79
84
  fireEvent.click(toggler);
80
85
  await waitFor(() => expect(collapse).not.toHaveClass('show'));
81
86
  });
87
+
88
+ test('tracks click-outs for GitHub project links', () => {
89
+ render(<AppNavBar />);
90
+ const githubProjectLink = screen.getByText('iRonoc-DB');
91
+ fireEvent.click(githubProjectLink);
92
+ expect(trackClickOut).toHaveBeenCalledWith('github', 'https://github.com/conorheffron/ironoc-db');
93
+ });
82
94
  });
83
95
 
84
96
  describe('Footer Component', () => {
package/src/AppNavbar.js CHANGED
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
2
2
  import "bootstrap/dist/css/bootstrap.min.css";
3
3
  import {Navbar, NavbarText, Container, Collapse, NavbarBrand, NavbarToggler, Nav, UncontrolledDropdown, DropdownToggle, DropdownItem, DropdownMenu } from "reactstrap";
4
4
  import logo from './img/robot-logo.png';
5
+ import { trackClickOut } from './utils/activityTracker';
5
6
  import "@fontsource/montserrat/700.css";
6
7
  import "@fontsource/open-sans/400-italic.css";
7
8
 
@@ -34,6 +35,10 @@ class AppNavBar extends Component {
34
35
  this.setState({ isOpen: !this.state.isOpen });
35
36
  };
36
37
 
38
+ handleGitHubClick = (target) => {
39
+ trackClickOut('github', target);
40
+ };
41
+
37
42
  render() {
38
43
  const { color, light, dark, fixed, container, expand, className, isOpen } = this.state;
39
44
 
@@ -76,12 +81,12 @@ class AppNavBar extends Component {
76
81
  <UncontrolledDropdown inNavbar nav>
77
82
  <DropdownToggle caret nav>GitHub Projects</DropdownToggle>
78
83
  <DropdownMenu end>
79
- <DropdownItem target="_blank" href="https://github.com/conorheffron/ironoc">iRonoc</DropdownItem>
84
+ <DropdownItem target="_blank" href="https://github.com/conorheffron/ironoc" onClick={() => this.handleGitHubClick('https://github.com/conorheffron/ironoc')}>iRonoc</DropdownItem>
80
85
  <DropdownItem divider />
81
- <DropdownItem target="_blank" href="https://github.com/conorheffron/ironoc-db">iRonoc-DB</DropdownItem>
82
- <DropdownItem target="_blank" href="https://github.com/conorheffron/booking-sys">Booking System Sample</DropdownItem>
83
- <DropdownItem target="_blank" href="https://github.com/conorheffron/nba-stats">NBA Stats Analysis</DropdownItem>
84
- <DropdownItem target="_blank" href="https://github.com/conorheffron/ironoc-pytest">PyTest GitHub Client Package</DropdownItem>
86
+ <DropdownItem target="_blank" href="https://github.com/conorheffron/ironoc-db" onClick={() => this.handleGitHubClick('https://github.com/conorheffron/ironoc-db')}>iRonoc-DB</DropdownItem>
87
+ <DropdownItem target="_blank" href="https://github.com/conorheffron/booking-sys" onClick={() => this.handleGitHubClick('https://github.com/conorheffron/booking-sys')}>Booking System Sample</DropdownItem>
88
+ <DropdownItem target="_blank" href="https://github.com/conorheffron/nba-stats" onClick={() => this.handleGitHubClick('https://github.com/conorheffron/nba-stats')}>NBA Stats Analysis</DropdownItem>
89
+ <DropdownItem target="_blank" href="https://github.com/conorheffron/ironoc-pytest" onClick={() => this.handleGitHubClick('https://github.com/conorheffron/ironoc-pytest')}>PyTest GitHub Client Package</DropdownItem>
85
90
  </DropdownMenu>
86
91
  </UncontrolledDropdown>
87
92
  <UncontrolledDropdown inNavbar nav>
@@ -8,6 +8,7 @@ import AppNavbar from '../AppNavbar';
8
8
  import red from '../img/red-bg.png';
9
9
  import { Container } from 'reactstrap';
10
10
  import LoadingSpinner from '.././LoadingSpinner';
11
+ import { trackClickOut } from '../utils/activityTracker';
11
12
 
12
13
  // Define the GraphQL query to fetch donate items
13
14
  export const GET_DONATE_ITEMS = gql`
@@ -78,6 +79,58 @@ class Donate extends Component {
78
79
  );
79
80
  }
80
81
 
82
+ return (
83
+ <div className="App">
84
+ <AppNavbar />
85
+ <Container>
86
+ <Carousel className="App-header">
87
+ {donateItems.map((item, index) => (
88
+ <Carousel.Item key={index} interval={500}>
89
+ <img className="d-block w-100" src={red} alt={item.alt} />
90
+
91
+ <Carousel.Caption>
92
+ <h1 className="mb-3">
93
+ <span style={{ textDecoration: 'underline' }}>{item.name}</span>
94
+ </h1>
95
+
96
+ <p>
97
+ <b>Contact & Help by Phone: </b>
98
+ <span dangerouslySetInnerHTML={{ __html: item.phone }} />
99
+ </p>
100
+
101
+ <p>
102
+ <b>Home page: </b>
103
+ <a
104
+ href={item.link}
105
+ target="_blank"
106
+ rel="noreferrer"
107
+ onClick={() => trackClickOut('charity', item.link)}
108
+ >
109
+ {item.link}
110
+ </a>
111
+ </p>
112
+
113
+ <p className="overview-text">
114
+ <b>Overview:</b> Founded in {item.founded}, {item.overview}
115
+ </p>
116
+
117
+ <p>
118
+ <a
119
+ href={item.donate}
120
+ target="_blank"
121
+ rel="noreferrer"
122
+ onClick={() => trackClickOut('charity', item.donate)}
123
+ >
124
+ Donate here
125
+ </a>
126
+ </p>
127
+ </Carousel.Caption>
128
+ </Carousel.Item>
129
+ ))}
130
+ </Carousel>
131
+ </Container>
132
+ </div>
133
+ );
81
134
  return (
82
135
  <div className="App">
83
136
  <AppNavbar />
@@ -6,7 +6,6 @@ import { useParams, useNavigate } from 'react-router';
6
6
  import {
7
7
  MaterialReactTable,
8
8
  useMaterialReactTable,
9
- type MRT_ColumnDef,
10
9
  } from 'material-react-table';
11
10
  import { darken, lighten, useTheme } from '@mui/material';
12
11
 
@@ -126,7 +125,13 @@ const RepoIssues = () => {
126
125
  data: repoIssueList,
127
126
  enableFacetedValues: true,
128
127
  enableStickyHeader: true,
129
- initialState: { showColumnFilters: true },
128
+ initialState: {
129
+ showColumnFilters: true,
130
+ columnVisibility: {
131
+ state: false,
132
+ body: false,
133
+ },
134
+ },
130
135
  muiTablePaperProps: {
131
136
  elevation: 0,
132
137
  sx: {
@@ -1,8 +1,13 @@
1
1
  import React from 'react';
2
- import { render, screen, waitFor } from '@testing-library/react';
2
+ import { render, screen, waitFor, fireEvent } from '@testing-library/react';
3
3
  import { MockedProvider } from '@apollo/client/testing';
4
4
  import Donate, { GET_DONATE_ITEMS } from '../Donate';
5
5
  import '@testing-library/jest-dom';
6
+ import { trackClickOut } from '../../utils/activityTracker';
7
+
8
+ jest.mock('../../utils/activityTracker', () => ({
9
+ trackClickOut: jest.fn(),
10
+ }));
6
11
 
7
12
  // Mock data for testing
8
13
  const mockDonateItems = [
@@ -94,4 +99,27 @@ describe('Donate Component', () => {
94
99
  });
95
100
  });
96
101
  });
102
+
103
+ it('tracks charity click-outs for donate and homepage links', async () => {
104
+ render(
105
+ <MockedProvider mocks={mocks}>
106
+ <Donate />
107
+ </MockedProvider>
108
+ );
109
+
110
+ await waitFor(() => {
111
+ expect(screen.getByText('Item 1')).toBeInTheDocument();
112
+ });
113
+
114
+ const donateLink = document.querySelector('a[href="https://example.com/donate1"]');
115
+ const homepageLink = document.querySelector('a[href="https://example.com/home1"]');
116
+ expect(donateLink).toBeInTheDocument();
117
+ expect(homepageLink).toBeInTheDocument();
118
+
119
+ fireEvent.click(donateLink);
120
+ fireEvent.click(homepageLink);
121
+
122
+ expect(trackClickOut).toHaveBeenCalledWith('charity', 'https://example.com/donate1');
123
+ expect(trackClickOut).toHaveBeenCalledWith('charity', 'https://example.com/home1');
124
+ });
97
125
  });
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
3
  import RepoIssues from '../RepoIssues';
4
+ import { useMaterialReactTable } from 'material-react-table';
4
5
 
5
6
  // Mock react-router
6
7
  jest.mock('react-router', () => ({
@@ -10,7 +11,11 @@ jest.mock('react-router', () => ({
10
11
 
11
12
  // Mock react-bootstrap
12
13
  jest.mock('react-bootstrap', () => ({
13
- Container: ({ children, ...props }) => <div data-testid="container" {...props}>{children}</div>,
14
+ Container: ({ children, fluid, ...props }) => (
15
+ <div data-testid="container" data-fluid={fluid ? 'true' : undefined} {...props}>
16
+ {children}
17
+ </div>
18
+ ),
14
19
  InputGroup: ({ children, ...props }) => <div data-testid="input-group" {...props}>{children}</div>,
15
20
  Form: {
16
21
  Control: ({ ...props }) => <input data-testid="form-control" {...props} />,
@@ -50,14 +55,23 @@ import { useParams, useNavigate } from 'react-router';
50
55
 
51
56
  describe('RepoIssues', () => {
52
57
  const mockNavigate = jest.fn();
58
+ const mockIssuesResponse = [];
53
59
 
54
60
  beforeEach(() => {
55
61
  jest.clearAllMocks();
56
62
  useNavigate.mockReturnValue(mockNavigate);
63
+ global.fetch = jest.fn(() =>
64
+ Promise.resolve({
65
+ json: () => Promise.resolve(mockIssuesResponse),
66
+ })
67
+ );
68
+ window.fetch = global.fetch;
57
69
  });
58
70
 
59
71
  it('shows loading spinner and navbar initially', () => {
60
72
  useParams.mockReturnValue({ id: 'user', repo: 'repo' });
73
+ global.fetch = jest.fn(() => new Promise(() => {}));
74
+ window.fetch = global.fetch;
61
75
  render(<RepoIssues />);
62
76
  expect(screen.getByTestId('navbar')).toBeInTheDocument();
63
77
  expect(screen.getByTestId('spinner')).toBeInTheDocument();
@@ -78,6 +92,7 @@ describe('RepoIssues', () => {
78
92
  ]),
79
93
  })
80
94
  );
95
+ window.fetch = global.fetch;
81
96
  render(<RepoIssues />);
82
97
  await waitFor(() =>
83
98
  expect(screen.queryByTestId('spinner')).not.toBeInTheDocument()
@@ -85,9 +100,30 @@ describe('RepoIssues', () => {
85
100
  expect(screen.getByTestId('mrt-table')).toBeInTheDocument();
86
101
  });
87
102
 
103
+ it('hides state and description columns by default', async () => {
104
+ useParams.mockReturnValue({ id: 'user', repo: 'repo' });
105
+ render(<RepoIssues />);
106
+ await waitFor(() =>
107
+ expect(screen.queryByTestId('spinner')).not.toBeInTheDocument()
108
+ );
109
+
110
+ expect(useMaterialReactTable).toHaveBeenCalledWith(
111
+ expect.objectContaining({
112
+ initialState: expect.objectContaining({
113
+ showColumnFilters: true,
114
+ columnVisibility: {
115
+ state: false,
116
+ body: false,
117
+ },
118
+ }),
119
+ })
120
+ );
121
+ });
122
+
88
123
  it('navigates on form submit', async () => {
89
124
  useParams.mockReturnValue({ id: 'user', repo: 'repo' });
90
125
  global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve([]) }));
126
+ window.fetch = global.fetch;
91
127
  render(<RepoIssues />);
92
128
  await waitFor(() => expect(screen.queryByTestId('spinner')).not.toBeInTheDocument());
93
129
  const input = screen.getByTestId('form-control');
@@ -0,0 +1,24 @@
1
+ export const trackClickOut = (category, target) => {
2
+ if (!category || !target) {
3
+ return;
4
+ }
5
+
6
+ const payload = JSON.stringify({ category, target });
7
+
8
+ try {
9
+ if (typeof navigator !== 'undefined' && navigator.sendBeacon) {
10
+ const blob = new Blob([payload], { type: 'application/json' });
11
+ navigator.sendBeacon('/api/activity/click-out', blob);
12
+ return;
13
+ }
14
+
15
+ fetch('/api/activity/click-out', {
16
+ method: 'POST',
17
+ headers: { 'Content-Type': 'application/json' },
18
+ body: payload,
19
+ keepalive: true
20
+ }).catch(() => null);
21
+ } catch {
22
+ // no-op to avoid blocking navigation
23
+ }
24
+ };