Skip to content

Commit

Permalink
CHAL-6 #done (#2)
Browse files Browse the repository at this point in the history
* scaffold next app

* added PR template, preliminary model, createNamedContext and useContextSafely hooks

* updated image locations

* implemented and tested useContextSafely

* migrated footer, page container and modal

* migrated header

* added home page

* implemented servicesContainer and ServicesContext, scaffolded LocalUserService

* simplified cb returned by useLayoutEffect hook in UserContextProvider

* reverted cb to arrow function in UserContextProvider

* updated github actions/checkout and setup-node to v4

* added local-user-service.ts to coveragePathIgnorePatterns array in jest.config.mjs

* fixed several merge conflicts, formatted code
  • Loading branch information
dvorakjt committed Mar 29, 2024
1 parent 5854218 commit 90192bc
Show file tree
Hide file tree
Showing 20 changed files with 348 additions and 37 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/run-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ jobs:
run-unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: npm ci
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Additionally, we will be using the following languages/frameworks/libraries, whi
- [TypeScript](https://www.typescriptlang.org/)
- [Firebase](https://firebase.google.com/)
- [Sass](https://sass-lang.com/)
- [Inversify](https://inversify.io/) - For dependency injection.
- [Luxon](https://github.com/moment/luxon/) - For manipulating dates.

Finally, please take some time to familiarize yourself with the different user stories depicted in the Figma.

Expand Down
9 changes: 7 additions & 2 deletions jest.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,20 @@ const config = {
'./src/stories',
'fonts',
],
//TODO : remove 'user-context.tsx' once it is implemented
/*
TODO : remove 'local-user-service.ts' once it is implemented. Then, tests
can be added for 'user-context.tsx' and it can be removed from this array
as well.
*/
coveragePathIgnorePatterns: [
'index.ts',
'index.tsx',
'layout.tsx',
'local-user-service.ts',
'user-context.tsx',
],
//require 100% code coverage for the tests to pass
coverageThreshold: {
//require 100% code coverage for the tests to pass
global: {
branches: 100,
functions: 100,
Expand Down
29 changes: 25 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@
"build-storybook": "storybook build"
},
"dependencies": {
"inversify": "^6.0.2",
"luxon": "^3.4.4",
"next": "14.1.4",
"react": "^18",
"react-dom": "^18",
"react-icons": "^5.0.1"
"react-icons": "^5.0.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@chromatic-com/storybook": "^1.2.25",
Expand Down
19 changes: 19 additions & 0 deletions src/__tests__/contexts/services-context.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
ServicesContext,
ServicesContextProvider,
} from '@/contexts/services-context';
import { getProvidedContextValue } from '@/testing-utils/get-provided-context-value';
import { AbstractUserService } from '@/services/classes/abstract/abstract-user-service';

describe('ServicesContext', () => {
it('provides an instance of AbstractUserService to its consumers.', () => {
const servicesContextValue = getProvidedContextValue(
ServicesContext,
ServicesContextProvider,
);
expect(servicesContextValue).not.toBeNull();
expect(servicesContextValue?.userService).toBeInstanceOf(
AbstractUserService,
);
});
});
35 changes: 35 additions & 0 deletions src/__tests__/services/classes/concrete/local-user-service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'reflect-metadata';
import { UserType } from '@/model/enums/user-type';
import { LocalUserService } from '@/services/classes/concrete/local-user-service';
import { Subscription } from 'rxjs';

describe('LocalUserService', () => {
//TODO : Remove this file from replace these tests as each method in LocalUserService is implemented.
test('It throws an error when the unimplemented signUpWithEmail() method is called.', () => {
const userService = new LocalUserService();
expect(() =>
userService.signUpWithEmail(
'[email protected]',
'user',
1,
UserType.Challenger,
),
).toThrow();
});

test('It throws an error when the unimplemented signInWithEmail() method is called.', () => {
const userService = new LocalUserService();
expect(() => userService.signInWithEmail('[email protected]')).toThrow();
});

test('It throws an error when the unimplemented signOut() method is called.', () => {
const userService = new LocalUserService();
expect(() => userService.signOut()).toThrow();
});

test('When its subscribe() method is called, an RxJS Subscription is returned.', () => {
const userService = new LocalUserService();
const subscription = userService.subscribe(() => {});
expect(subscription).toBeInstanceOf(Subscription);
});
});
12 changes: 12 additions & 0 deletions src/__tests__/services/services-container.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { servicesContainer } from '@/services/services-container';
import { TYPES } from '@/services/types';
import { AbstractUserService } from '@/services/classes/abstract/abstract-user-service';

describe('servicesContainer', () => {
it('Provides a subclass of the AbstractUserService service class.', () => {
const userService = servicesContainer.get<AbstractUserService>(
TYPES.UserService,
);
expect(userService).toBeInstanceOf(AbstractUserService);
});
});
13 changes: 8 additions & 5 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from 'next';
import '../styles/main.scss';
import { bebasNeue } from '@/fonts/bebas-neue';
import { lato } from '@/fonts/lato';
import { ServicesContextProvider } from '@/contexts/services-context';
import { UserContextProvider } from '@/contexts/user-context';
import { Header } from '@/components/header';
import { Footer } from '@/components/footer';
Expand All @@ -19,11 +20,13 @@ export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en">
<body className={`${bebasNeue.variable} ${lato.variable}`}>
<UserContextProvider>
<Header />
{children}
<Footer />
</UserContextProvider>
<ServicesContextProvider>
<UserContextProvider>
<Header />
{children}
<Footer />
</UserContextProvider>
</ServicesContextProvider>
</body>
</html>
);
Expand Down
25 changes: 25 additions & 0 deletions src/contexts/services-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use client';
import { servicesContainer } from '@/services/services-container';
import { createNamedContext } from '@/hooks/functions/create-named-context';
import { TYPES } from '@/services/types';
import type { AbstractUserService } from '@/services/classes/abstract/abstract-user-service';
import type { PropsWithChildren } from 'react';

interface ServicesContextType {
userService: AbstractUserService;
}

export const ServicesContext =
createNamedContext<ServicesContextType>('ServicesContext');

export function ServicesContextProvider({ children }: PropsWithChildren) {
const userService = servicesContainer.get<AbstractUserService>(
TYPES.UserService,
);

return (
<ServicesContext.Provider value={{ userService }}>
{children}
</ServicesContext.Provider>
);
}
36 changes: 34 additions & 2 deletions src/contexts/user-context.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,49 @@
'use client';
import { PropsWithChildren } from 'react';
import { useState, useLayoutEffect, type PropsWithChildren } from 'react';
import { createNamedContext } from '../hooks/functions/create-named-context';
import { useContextSafely } from '@/hooks/functions/use-context-safely';
import { ServicesContext } from './services-context';
import type { User } from '../model/types/user';
import type { Avatar } from '@/model/types/avatar.type';
import type { UserType } from '@/model/enums/user-type';

interface UserContextType {
user: User | null;
signUpWithEmail(
email: string,
name: string,
avatar: Avatar,
type: UserType,
): Promise<void>;
signInWithEmail(email: string): Promise<void>;
signOut(): void;
}

const UserContext = createNamedContext<UserContextType>('UserContext');

function UserContextProvider({ children }: PropsWithChildren) {
const { userService } = useContextSafely(
ServicesContext,
'UserContextProvider',
);
const [user, setUser] = useState<User | null>(userService.user);

useLayoutEffect(() => {
const subscription = userService.subscribe((user: User | null) =>
setUser(user),
);
return () => subscription.unsubscribe();
});

return (
<UserContext.Provider value={{ user: null }}>
<UserContext.Provider
value={{
signUpWithEmail: userService.signUpWithEmail,
signInWithEmail: userService.signInWithEmail,
signOut: userService.signOut,
user,
}}
>
{children}
</UserContext.Provider>
);
Expand Down
7 changes: 6 additions & 1 deletion src/model/types/badge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ import { Avatar } from './avatar.type';
/**
* Represents a badge awarded to the user either through their own actions, or
* due to an action taken by a player they invited.
*
* @remarks
* The two actions that grant the challenger themselves a badge are registering
* to vote and sharing the challenger. Signing up for election reminders does
* not grant the user a badge.
*/
export type Badge =
| {
action: Actions;
action: Actions.VoterRegistration | Actions.SharedChallenge;
}
| {
playerName: string;
Expand Down
10 changes: 10 additions & 0 deletions src/model/types/challenger.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Avatar } from './avatar.type';

/**
* Represents data about a challenger that is stored in a player's user document.
*/
export interface Challenger {
uid: string;
name: string;
avatar: Avatar;
}
30 changes: 11 additions & 19 deletions src/model/types/user.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { UserType } from '../enums/user-type';
import type { Avatar } from './avatar.type';
import type { Badge } from './badge';
import type { Challenger } from './challenger.type';

export interface User {
uid: string;
Expand All @@ -18,30 +19,21 @@ export interface User {
completedChallenge: boolean;
redeemedAward: boolean;
/**
* If the user is a player (a user of type UserType.Player or
* UserType.Hybrid), this object contains information about the challenger
* that referred them.
* An array of the challengers whose challenges a player has contributed to
* by taking an action.
*
* @remarks
* The uid field will be used to award the challenger a badge when the
* player completes an action. The name and avatar fields are
* used to display the name and avatar of the challenger within the UI
* for the player once the player has completed an action towards the
* challenger's challenge.
* The names and avatars of these challengers are displayed on
* the /actions page once the player has completed all possible actions.
*/
invitedBy?: {
uid: string;
name: string;
avatar: Avatar;
};
contributedTo: Challenger[];
/**
* Represents whether a player (a user of type UserType.Player or
* UserType.Hybrid) has taken an action on behalf of a challenger.
* The most recent challenger to invite the player.
*
* @remarks
* Each player may only take one action on behalf of one challenger. If this
* field is `true`, any subsequent actions taken by the user will not award
* that challenger a badge.
* Each time the challenger clicks a new challenger's share link, this will be
* updated.
*/
completedActionForChallenger: boolean;
invitedBy?: Challenger;
shareCode: string;
}
Loading

0 comments on commit 90192bc

Please sign in to comment.