feat: calendar matchings (#173)
* feat: calendar matchings * fix: build * refactor: resolve pr comments * fix: destrucure editorRef --------- Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
This commit is contained in:
@@ -46,7 +46,7 @@ export default function Dialog(props: PropsWithChildren<DialogProps>): JSX.Eleme
|
||||
<div className='fixed inset-0 z-50 flex items-center justify-center'>
|
||||
<HDialog.Panel
|
||||
className={clsx(
|
||||
'z-99 max-h-[90vh] flex flex-col overflow-y-auto border border-ut-offwhite rounded-lg bg-white shadow-xl ml-[calc(100vw-100%)] mt-[calc(100vw-100%)]',
|
||||
'z-99 max-h-[90vh] flex flex-col overflow-y-auto border border-solid border-ut-offwhite rounded bg-white shadow-xl ml-[calc(100vw-100%)] mt-[calc(100vw-100%)]',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -3,18 +3,21 @@ import 'uno.css';
|
||||
|
||||
import type TabInfoMessages from '@shared/messages/TabInfoMessages';
|
||||
import { MessageListener } from 'chrome-extension-toolkit';
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import styles from './ExtensionRoot.module.scss';
|
||||
|
||||
interface Props {
|
||||
testId?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper component for the extension elements that adds some basic styling to them
|
||||
*/
|
||||
export default function ExtensionRoot(props: React.PropsWithChildren<Props>): JSX.Element {
|
||||
// TODO: move out of ExtensionRoot
|
||||
useEffect(() => {
|
||||
const tabInfoListener = new MessageListener<TabInfoMessages>({
|
||||
getTabInfo: ({ sendResponse }) => {
|
||||
@@ -31,7 +34,7 @@ export default function ExtensionRoot(props: React.PropsWithChildren<Props>): JS
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.extensionRoot} data-testid={props.testId}>
|
||||
<div className={clsx(styles.extensionRoot, props.className)} data-testid={props.testId}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { SVGProps } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
export function LogoIcon(props: SVGProps<SVGSVGElement>) {
|
||||
export function LogoIcon(props: SVGProps<SVGSVGElement>): JSX.Element {
|
||||
return (
|
||||
<svg width='40' height='40' viewBox='0 0 40 40' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<svg width='40' height='40' viewBox='0 0 40 40' fill='none' xmlns='http://www.w3.org/2000/svg' {...props}>
|
||||
<circle cx='20' cy='20' r='20' fill='#BF5700' />
|
||||
<circle cx='20' cy='20' r='15.5' stroke='white' strokeWidth='3' />
|
||||
<rect x='18' y='10' width='4' height='19.5489' fill='white' />
|
||||
@@ -11,3 +11,27 @@ export function LogoIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function SmallLogo(): JSX.Element {
|
||||
return (
|
||||
<div className='flex items-center gap-2'>
|
||||
<LogoIcon />
|
||||
<div className='flex flex-col text-lg font-medium leading-[1em]'>
|
||||
<p className='text-ut-burntorange'>UT Registration</p>
|
||||
<p className='text-ut-orange'>Plus</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LargeLogo(): JSX.Element {
|
||||
return (
|
||||
<div className='flex items-center gap-2'>
|
||||
<LogoIcon className='h-12 w-12' />
|
||||
<div className='flex flex-col text-[1.35rem] font-medium leading-[1em]'>
|
||||
<p className='text-ut-burntorange'>UT Registration</p>
|
||||
<p className='text-ut-orange'>Plus</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import deleteSchedule from '@pages/background/lib/deleteSchedule';
|
||||
import renameSchedule from '@pages/background/lib/renameSchedule';
|
||||
import type { UserSchedule } from '@shared/types/UserSchedule';
|
||||
import Text from '@views/components/common/Text/Text';
|
||||
import useSchedules from '@views/hooks/useSchedules';
|
||||
import clsx from 'clsx';
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import XIcon from '~icons/material-symbols/close';
|
||||
import DragIndicatorIcon from '~icons/material-symbols/drag-indicator';
|
||||
|
||||
/**
|
||||
@@ -19,31 +22,80 @@ export type Props = {
|
||||
/**
|
||||
* This is a reusable dropdown component that can be used to toggle the visiblity of information
|
||||
*/
|
||||
export default function ScheduleListItem({ style, schedule, dragHandleProps, onClick }: Props): JSX.Element {
|
||||
export default function ScheduleListItem({ schedule, dragHandleProps, onClick }: Props): JSX.Element {
|
||||
const [activeSchedule] = useSchedules();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editorValue, setEditorValue] = useState(schedule.name);
|
||||
|
||||
const editorRef = React.useRef<HTMLInputElement>(null);
|
||||
const { current: editor } = editorRef;
|
||||
useEffect(() => {
|
||||
setEditorValue(schedule.name);
|
||||
|
||||
if (isEditing && editor) {
|
||||
editor.focus();
|
||||
editor.setSelectionRange(0, editor.value.length);
|
||||
}
|
||||
}, [isEditing, schedule.name, editor]);
|
||||
|
||||
const isActive = useMemo(() => activeSchedule.id === schedule.id, [activeSchedule, schedule]);
|
||||
|
||||
const handleBlur = () => {
|
||||
if (editorValue.trim() !== '') {
|
||||
schedule.name = editorValue.trim();
|
||||
renameSchedule(schedule.id, schedule.name);
|
||||
}
|
||||
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ ...style }} className='items-center rounded bg-white'>
|
||||
<li className='w-100% flex cursor-pointer items-center self-stretch justify-left text-ut-burntorange'>
|
||||
<div className='flex justify-center'>
|
||||
<div className='rounded bg-white'>
|
||||
<li className='w-full flex cursor-pointer items-center text-ut-burntorange'>
|
||||
<div className='h-full cursor-move focusable' {...dragHandleProps}>
|
||||
<DragIndicatorIcon className='h-6 w-6 cursor-move text-zinc-300 btn-transition -ml-1.5 hover:text-zinc-400' />
|
||||
</div>
|
||||
<div className='group flex flex-1 items-center overflow-x-hidden'>
|
||||
<div
|
||||
className='flex cursor-move items-center self-stretch rounded rounded-r-0'
|
||||
{...dragHandleProps}
|
||||
className='flex flex-grow items-center gap-1.5 overflow-x-hidden'
|
||||
onClick={(...e) => !isEditing && onClick(...e)}
|
||||
>
|
||||
<DragIndicatorIcon className='h-6 w-6 cursor-move text-zinc-300 btn-transition -ml-1.5 hover:text-zinc-400' />
|
||||
</div>
|
||||
<div className='group inline-flex items-center justify-center gap-1.5' onClick={onClick}>
|
||||
<div
|
||||
className={clsx(
|
||||
'h-5.5 w-5.5 relative border-2px border-current rounded-full btn-transition group-active:scale-95 after:(absolute content-empty bg-current h-2.9 w-2.9 rounded-full transition tansform scale-100 ease-out-expo duration-250 -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2)',
|
||||
'h-5.5 w-5.5 relative border-2px border-current rounded-full btn-transition group-active:scale-95 after:(absolute content-empty bg-current h-2.9 w-2.9 rounded-full transition tansform-gpu scale-100 ease-out-expo duration-250 -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2)',
|
||||
{
|
||||
'after:(scale-0! opacity-0 ease-in-out! duration-200!)': !isActive,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<Text variant='p'>{schedule.name}</Text>
|
||||
{isEditing && (
|
||||
<Text
|
||||
variant='p'
|
||||
as='input'
|
||||
className='mr-1 flex-1 px-0.5 outline-blue-500 -ml-0.5'
|
||||
value={editorValue}
|
||||
onChange={e => setEditorValue(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') handleBlur();
|
||||
if (e.key === 'Escape') {
|
||||
setIsEditing(false);
|
||||
}
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
ref={editorRef}
|
||||
/>
|
||||
)}
|
||||
{!isEditing && (
|
||||
<Text variant='p' className='flex-1 truncate' onDoubleClick={() => setIsEditing(true)}>
|
||||
{schedule.name}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<XIcon
|
||||
className='invisible h-5 w-5 text-ut-red group-hover:visible'
|
||||
onClick={() => deleteSchedule(schedule.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -21,14 +21,14 @@ export default function ScheduleTotalHoursAndCourses({
|
||||
totalCourses,
|
||||
}: ScheduleTotalHoursAndCoursesProps): JSX.Element {
|
||||
return (
|
||||
<div className='min-w-64 flex content-center items-baseline gap-2 whitespace-nowrap uppercase'>
|
||||
<Text className='text-ut-burntorange' variant='h1' as='span'>
|
||||
<div className='min-w-64 flex items-center gap-2.5 whitespace-nowrap'>
|
||||
<Text className='text-ut-burntorange uppercase' variant='h1' as='span'>
|
||||
{`${scheduleName}: `}
|
||||
</Text>
|
||||
<Text variant='h3' as='div' className='flex flex-row items-center gap-2 text-theme-black'>
|
||||
{totalHours} {totalHours === 1 ? 'HOUR' : 'HOURS'}
|
||||
<Text variant='h4' as='span' className='text-ut-black'>
|
||||
{totalCourses} {totalCourses === 1 ? 'COURSE' : 'COURSES'}
|
||||
{totalHours} {totalHours === 1 ? 'Hour' : 'Hours'}
|
||||
<Text variant='h4' as='span' className='text-ut-black capitalize'>
|
||||
{totalCourses} {totalCourses === 1 ? 'Course' : 'Courses'}
|
||||
</Text>
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
.mini {
|
||||
font-size: 0.79rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 0.88875rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.p {
|
||||
@@ -25,11 +27,13 @@
|
||||
.h4 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.h3-course {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
line-height: 100%; /* 0.6875rem */
|
||||
}
|
||||
|
||||
@@ -37,6 +41,7 @@
|
||||
font-size: 1.26563rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.h2-course {
|
||||
@@ -49,16 +54,19 @@
|
||||
.h2 {
|
||||
font-size: 1.42375rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.h1-course {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.h1 {
|
||||
font-size: 1.60188rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PropsOf, ReactTag } from '@headlessui/react/dist/types';
|
||||
import clsx from 'clsx';
|
||||
import type { ElementType, ReactNode } from 'react';
|
||||
import type { ElementType, ReactNode, Ref } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import styles from './Text.module.scss';
|
||||
@@ -13,6 +13,7 @@ type CleanProps<TTag extends ReactTag, TOmitableProps extends PropertyKey = neve
|
||||
type OurProps<TTag extends ReactTag> = {
|
||||
as?: TTag;
|
||||
children?: ReactNode;
|
||||
ref?: React.ForwardedRef<React.ElementRef<TTag>>;
|
||||
};
|
||||
|
||||
type AsProps<TTag extends ReactTag, TOverrides = {}> = CleanProps<TTag, keyof TOverrides> & OurProps<TTag> & TOverrides;
|
||||
@@ -36,14 +37,14 @@ export type TextProps<TTag extends ElementType = 'span'> = PropsOf<TTag>['classN
|
||||
/**
|
||||
* A reusable Text component with props that build on top of the design system for the extension
|
||||
*/
|
||||
export default function Text<TTag extends ElementType = 'span'>({
|
||||
as,
|
||||
className,
|
||||
variant,
|
||||
...rest
|
||||
}: TextProps<TTag>): JSX.Element {
|
||||
function Text<TTag extends ElementType = 'span'>(
|
||||
{ as, className, variant, ...rest }: TextProps<TTag>,
|
||||
ref: Ref<HTMLElement>
|
||||
): JSX.Element {
|
||||
const Comp = as || 'span';
|
||||
const mergedClassName = clsx(styles.text, styles[variant || 'p'], className);
|
||||
|
||||
return <Comp className={mergedClassName} {...rest} />;
|
||||
return <Comp className={mergedClassName} {...rest} ref={ref} />;
|
||||
}
|
||||
|
||||
export default React.forwardRef(Text) as typeof Text;
|
||||
|
||||
Reference in New Issue
Block a user