Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25,275 changes: 25,275 additions & 0 deletions GenevieveCapolongo.patch

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions app/controllers/api_controller.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
# frozen_string_literal: true

class ApiController < ApplicationController
protect_from_forgery with: :null_session

def validate_account
username = params[:username]
password = params[:password]

username_result = User.validate_username(username)
password_result = User.validate_password(password)

render json: {
username: username_result,
password: password_result
}
end
end
2 changes: 2 additions & 0 deletions app/frontend/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const SIGN_UP_ERROR_MESSAGE = 'Unable to access this page. Please create an account first.';
export const SERVER_ERROR_MESSAGE = 'Something went wrong. Please try again later.';
21 changes: 21 additions & 0 deletions app/frontend/cookies/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Setting a cookie
export const setCookie = (name: string, value: string): void => {
document.cookie = name + '=' + (value || '') + '; Path=/;';
};

// Getting a cookie
export const getCookie = (name: string): string | undefined => {
const nameEQ = name + '=';
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return undefined;
};

// Deleting a cookie
export const eraseCookie = (name: string): void => {
document.cookie = name + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
};
8 changes: 7 additions & 1 deletion app/frontend/entrypoints/main.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Router } from '../router.tsx';
import { AccountProvider } from '../providers/AccountProvider/index.tsx';
import '../tailwind.css';
import { AlertProvider } from '../providers/AlertProvider/index.tsx';

ReactDOM.createRoot(document.getElementById('vite-app')!).render(
<React.StrictMode>
<Router />
<AccountProvider>
<AlertProvider>
<Router />
</AlertProvider>
</AccountProvider>
</React.StrictMode>
);
17 changes: 17 additions & 0 deletions app/frontend/providers/AccountProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { createContext, useReducer } from 'react';
import { accountReducer, AnonymousUser } from './reducer';
import { AccountStateType, AccountDispatchType, AccountProviderProps } from './types';

export const AccountContext = createContext<
| {
state: AccountStateType;
dispatch: AccountDispatchType; // connect the action to the state
}
| undefined
>(undefined);

export function AccountProvider({ children }: AccountProviderProps) {
const [state, dispatch] = useReducer(accountReducer, AnonymousUser);
const value = { state, dispatch };
return <AccountContext.Provider value={value}>{children}</AccountContext.Provider>;
}
27 changes: 27 additions & 0 deletions app/frontend/providers/AccountProvider/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Reducer } from 'react';
import { AccountActions, AccountActionTypes } from './types';
import { AccountStateType } from './types';

export const AnonymousUser = {
username: '',
isValid: false,
};

export const accountReducer: Reducer<AccountStateType, AccountActions> = (state, action) => {
switch (action.type) {
case AccountActionTypes.SET_USERNAME: {
return {
...state,
username: action.payload?.username,
isValid: true,
};
}
case AccountActionTypes.DELETE_USERNAME: {
return {
...state,
username: '',
isValid: false,
};
}
}
};
31 changes: 31 additions & 0 deletions app/frontend/providers/AccountProvider/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ReactNode } from 'react';

export type AccountStateType = {
username: string;
isValid: boolean;
};

// a function that takes in an action
export type AccountDispatchType = (action: AccountActions) => void;

export type AccountProviderProps = {
children: ReactNode;
};

export enum AccountActionTypes {
SET_USERNAME = 'SET_USERNAME',
DELETE_USERNAME = 'DELETE_USERNAME',
}

interface SetUsername {
type: typeof AccountActionTypes.SET_USERNAME;
payload: {
username: string;
};
}

interface DeleteUsername {
type: typeof AccountActionTypes.DELETE_USERNAME;
}

export type AccountActions = SetUsername | DeleteUsername;
17 changes: 17 additions & 0 deletions app/frontend/providers/AlertProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { createContext, useReducer } from 'react';
import { alertReducer, EmptyAlert } from './reducer';
import { AlertStateType, AlertDispatchType, AlertProviderProps } from './types';

export const AlertContext = createContext<
| {
state: AlertStateType;
dispatch: AlertDispatchType; // connect the action to the state
}
| undefined
>(undefined);

export function AlertProvider({ children }: AlertProviderProps) {
const [state, dispatch] = useReducer(alertReducer, EmptyAlert);
const value = { state, dispatch };
return <AlertContext.Provider value={value}>{children}</AlertContext.Provider>;
}
23 changes: 23 additions & 0 deletions app/frontend/providers/AlertProvider/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Reducer } from 'react';
import { AlertActions, AlertActionTypes, AlertStateType } from './types';

export const EmptyAlert = {
alert: { message: '' },
};

export const alertReducer: Reducer<AlertStateType, AlertActions> = (state, action) => {
switch (action.type) {
case AlertActionTypes.SET_ALERT: {
return {
...state,
alert: action.payload?.alert,
};
}
case AlertActionTypes.DELETE_ALERT: {
return {
...state,
alert: { message: '' },
};
}
}
};
33 changes: 33 additions & 0 deletions app/frontend/providers/AlertProvider/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ReactNode } from 'react';

type AlertType = {
message: string;
};

export type AlertStateType = {
alert: AlertType;
};

export type AlertDispatchType = (action: AlertActions) => void;

export type AlertProviderProps = {
children: ReactNode;
};

export enum AlertActionTypes {
SET_ALERT = 'SET_ALERT',
DELETE_ALERT = 'DELETE_ALERT',
}

interface SetAlert {
type: typeof AlertActionTypes.SET_ALERT;
payload: {
alert: AlertType;
};
}

interface DeleteAlert {
type: typeof AlertActionTypes.DELETE_ALERT;
}

export type AlertActions = SetAlert | DeleteAlert;
28 changes: 28 additions & 0 deletions app/frontend/reusable-components/alert/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';

interface AlertProps {
message?: string;
}

export const Alert = ({ message }: AlertProps) => {
if (!message) return null;

return (
<div className="flex items-center gap-2 p-3 bg-yellow-50 border-l-4 border-yellow-400 text-yellow-800 rounded">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 text-yellow-500 flex-shrink-0"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M8.257 3.099c.764-1.36 2.722-1.36 3.486 0l6.518 11.614c.75 1.338-.213 3.037-1.742 3.037H3.48c-1.53 0-2.492-1.7-1.742-3.037L8.257 3.1zM11 13a1 1 0 10-2 0 1 1 0 002 0zm-1-2a1 1 0 01-1-1V8a1 1 0 112 0v2a1 1 0 01-1 1z"
clipRule="evenodd"
/>
</svg>
<span>{message}</span>
</div>
);
};
41 changes: 30 additions & 11 deletions app/frontend/reusable-components/button/button.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,44 @@
import React, { ReactNode } from 'react';
import React, { useMemo } from 'react';
import { Link } from 'react-router-dom';
import { ButtonProps, ButtonVariants } from './types';
import classNames from 'classnames';

interface Props {
type?: 'button' | 'submit';
href?: string;
children: ReactNode;
}

const classes = 'inline-block py-3 px-6 bg-[hsla(244,49%,49%,1)] text-white';
export function Button({ href, children, type, variant, isFullWidth, onClick, disabled }: ButtonProps) {
const buttonStyling = useMemo(() => {
switch (variant) {
case ButtonVariants.PRIMARY:
return 'py-3 px-6 bg-[hsla(244,49%,49%,1)] text-white rounded-[8px]';
case ButtonVariants.SECONDARY:
return 'py-3 px-6 bg-white text-[hsla(244,49%,49%,1)] rounded-[8px]';
case ButtonVariants.TERTIARY:
return 'pt-1 pr-2 border-b-4 border-solid border-slate-500 text-slate-500';
default:
return 'py-3 px-6 bg-[hsla(244,49%,49%,1)] text-white rounded-[8px]';
}
}, []);

export function Button({ href, children, type }: Props) {
if (href) {
return (
<Link to={href} className={classes}>
<Link
to={href}
className={classNames('inline-block', buttonStyling, {
'w-full text-center': isFullWidth,
})}
>
{children}
</Link>
);
}

return (
<button type={type} className={classes}>
<button
type={type}
className={classNames('inline-block ', buttonStyling, {
'w-full text-center': isFullWidth,
})}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
Expand Down
19 changes: 19 additions & 0 deletions app/frontend/reusable-components/button/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ReactNode } from 'react';

export interface ButtonProps {
type?: 'button' | 'submit';
href?: string;
children: ReactNode;
isFullWidth?: boolean;
variant?: VariantTypes | undefined;
onClick?: () => void;
disabled?: boolean;
}

export enum ButtonVariants {
PRIMARY = 'primary',
SECONDARY = 'secondary',
TERTIARY = 'tertiary',
}

export type VariantTypes = ButtonVariants.PRIMARY | ButtonVariants.SECONDARY | ButtonVariants.TERTIARY;
14 changes: 11 additions & 3 deletions app/frontend/reusable-components/card/card.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import classNames from 'classnames';
import React, { ReactNode } from 'react';

interface Props {
children: ReactNode;
title: string;
description?: string;
isFullWidth?: boolean;
titleStyles?: string;
}

export function Card({ children, title, description }: Props) {
export function Card({ children, title, description, isFullWidth, titleStyles }: Props) {
return (
<section className="p-10 shadow-card min-h-[400px] w-full rounded-2xl border border-solid border-slate-200">
<h1 className="text-2xl font-medium m-0 mb-1">{title}</h1>
<section
className={classNames('p-10 shadow-card min-h-[400px] rounded-2xl border border-solid border-slate-200', {
'w-full': isFullWidth,
'w-[450px] mx-auto': !isFullWidth,
})}
>
<h1 className={classNames('text-2xl font-medium m-0 mb-1', titleStyles)}>{title}</h1>
<p className="text-[hsla(243,30%,13%,.63)] text-base m-0 mb-1">{description}</p>
{children}
</section>
Expand Down
30 changes: 24 additions & 6 deletions app/frontend/reusable-components/flow-layout/flow-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
import React, { ReactNode } from 'react';
import { Link } from 'react-router-dom';
import { AccountContext } from 'app/frontend/providers/AccountProvider';
import { AccountActionTypes } from 'app/frontend/providers/AccountProvider/types';
import React, { ReactNode, useCallback, useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from '../button/button';
import { ButtonVariants } from '../button/types';
import { getCookie, eraseCookie } from 'app/frontend/cookies/helpers';

interface Props {
children: ReactNode;
}

export function FlowLayout({ children }: Props) {
const user = useContext(AccountContext);
const navigate = useNavigate();
const sessionToken = getCookie('session_token');
const isValid = user?.state.isValid || Boolean(sessionToken);

const handleLogout = useCallback(() => {
user?.dispatch({ type: AccountActionTypes.DELETE_USERNAME });
eraseCookie('session_token');
navigate('/create-account');
}, [user, navigate]);

return (
<div className="h-full mt-5 max-w-[1000px] mx-auto">
<div className="h-full mt-5 max-w-[1000px]">
<div className="w-full text-right">
<Link to="/logout" reloadDocument>
Logout
</Link>
{isValid && (
<Button onClick={handleLogout} variant={ButtonVariants.TERTIARY} type="button">
Logout
</Button>
)}
</div>
{children}
</div>
Expand Down
Loading