-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: export/import functionality (backup/restore/share with friends)…
… + a new input component (#433) * feat: export schedule function to be added to handler * feat: use UserScheduleStore and return json * feat: download functionality * feat: oh wow we already have a blob download util that is very very nice * feat: return empty json if none found * feat: import function completion * feat: file uploading done * feat: new input component-stories made-settings input replaced with component * feat: attempt 1 to hook settings.tsx to importSchedule * feat: it works horray aka using right Course constructor it works * chore: fix jsdoc * chore: comments and debug style * docs: extra comment * feat: name of schedule more user friendly * feat: reworked how schedule is passed and check for file being schedule * feat: color is kept on import * fix: add sendResponse to exportSchedule --------- Co-authored-by: doprz <[email protected]>
- Loading branch information
Showing
8 changed files
with
329 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; | ||
|
||
/** | ||
* Exports the provided schedule to a portable JSON | ||
* | ||
* @param scheduleId - The Id matching the to-be-exported schedule | ||
* @returns JSON format of the provided schedule ID, empty if one was not found | ||
*/ | ||
export default async function exportSchedule(scheduleId: string): Promise<string | undefined> { | ||
try { | ||
const storageData = await UserScheduleStore.get('schedules'); | ||
const selectedSchedule = storageData.find(s => s.id === scheduleId); | ||
|
||
if (!selectedSchedule) { | ||
console.warn(`Schedule ${scheduleId} does not exist`); | ||
return JSON.stringify({}); | ||
} | ||
|
||
console.log(selectedSchedule); | ||
return JSON.stringify(selectedSchedule, null, 2); | ||
} catch (error) { | ||
console.error('Error getting storage data:', error); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { Course } from '@shared/types/Course'; | ||
import type { UserSchedule } from '@shared/types/UserSchedule'; | ||
import type { Serialized } from 'chrome-extension-toolkit'; | ||
|
||
import addCourse from './addCourse'; | ||
import createSchedule from './createSchedule'; | ||
import switchSchedule from './switchSchedule'; | ||
|
||
function isValidSchedule(data: unknown): data is Serialized<UserSchedule> { | ||
if (typeof data !== 'object' || data === null) return false; | ||
const schedule = data as Record<string, unknown>; | ||
return typeof schedule.id === 'string' && typeof schedule.name === 'string' && Array.isArray(schedule.courses); | ||
} | ||
|
||
/** | ||
* Imports a user schedule from portable file, creating a new schedule for it | ||
* @param scheduleData - Data to be parsed back into a course schedule | ||
*/ | ||
export default async function importSchedule(scheduleData: unknown): Promise<void> { | ||
if (isValidSchedule(scheduleData)) { | ||
const newScheduleId = await createSchedule(scheduleData.name); | ||
await switchSchedule(newScheduleId); | ||
|
||
for (const c of scheduleData.courses) { | ||
const course = new Course(c); | ||
// eslint-disable-next-line no-await-in-loop | ||
await addCourse(newScheduleId, course, true); | ||
console.log(course.colors); | ||
} | ||
console.log('Course schedule successfully parsed!'); | ||
} else { | ||
console.error('No schedule data provided for import'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { colorsFlattened } from '@shared/util/themeColors'; | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import InputButton from '@views/components/common/InputButton'; | ||
import React from 'react'; | ||
|
||
import AddIcon from '~icons/material-symbols/add'; | ||
import CalendarMonthIcon from '~icons/material-symbols/calendar-month'; | ||
import DescriptionIcon from '~icons/material-symbols/description'; | ||
import ImagePlaceholderIcon from '~icons/material-symbols/image'; | ||
import HappyFaceIcon from '~icons/material-symbols/mood'; | ||
import ReviewsIcon from '~icons/material-symbols/reviews'; | ||
|
||
/** | ||
* Stole this straight from Button.stories.tsx to test the input | ||
*/ | ||
const meta = { | ||
title: 'Components/Common/InputButton', | ||
component: InputButton, | ||
parameters: { | ||
layout: 'centered', | ||
}, | ||
tags: ['autodocs'], | ||
args: { | ||
children: 'Upload File', | ||
icon: ImagePlaceholderIcon, | ||
}, | ||
argTypes: { | ||
children: { control: 'text' }, | ||
color: { | ||
control: 'select', | ||
options: Object.keys(colorsFlattened), | ||
}, | ||
variant: { | ||
control: 'select', | ||
options: ['filled', 'outline', 'single'], | ||
}, | ||
disabled: { | ||
control: 'boolean', | ||
}, | ||
onChange: { action: 'file selected' }, // action to show when file is selected | ||
}, | ||
} satisfies Meta<typeof InputButton>; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Default: Story = { | ||
args: { | ||
variant: 'filled', | ||
color: 'ut-black', // Default theme color | ||
}, | ||
}; | ||
|
||
export const Disabled: Story = { | ||
args: { | ||
variant: 'filled', | ||
color: 'ut-black', | ||
disabled: true, | ||
}, | ||
}; | ||
|
||
// @ts-ignore | ||
export const Grid: Story = { | ||
render: props => ( | ||
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}> | ||
<div style={{ display: 'flex', gap: '15px' }}> | ||
<InputButton {...props} variant='filled' color='ut-black' /> | ||
<InputButton {...props} variant='outline' color='ut-black' /> | ||
<InputButton {...props} variant='single' color='ut-black' /> | ||
</div> | ||
|
||
<hr /> | ||
|
||
<div style={{ display: 'flex', gap: '15px' }}> | ||
<InputButton {...props} variant='filled' color='ut-black' disabled /> | ||
<InputButton {...props} variant='outline' color='ut-black' disabled /> | ||
<InputButton {...props} variant='single' color='ut-black' disabled /> | ||
</div> | ||
</div> | ||
), | ||
}; | ||
|
||
export const PrettyColors: Story = { | ||
// @ts-ignore | ||
args: { | ||
children: '', | ||
}, | ||
render: props => { | ||
const colorsNames = Object.keys(colorsFlattened) as (keyof typeof colorsFlattened)[]; | ||
|
||
return ( | ||
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}> | ||
{colorsNames.map(color => ( | ||
<div style={{ display: 'flex', gap: '15px' }} key={color}> | ||
<InputButton {...props} variant='filled' color={color}> | ||
Button | ||
</InputButton> | ||
<InputButton {...props} variant='outline' color={color}> | ||
Button | ||
</InputButton> | ||
<InputButton {...props} variant='single' color={color}> | ||
Button | ||
</InputButton> | ||
</div> | ||
))} | ||
</div> | ||
); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import type { ThemeColor } from '@shared/types/ThemeColors'; | ||
import { getThemeColorHexByName, getThemeColorRgbByName } from '@shared/util/themeColors'; | ||
import Text from '@views/components/common/Text/Text'; | ||
import clsx from 'clsx'; | ||
import React from 'react'; | ||
|
||
import type IconComponent from '~icons/material-symbols'; | ||
|
||
interface Props { | ||
className?: string; | ||
style?: React.CSSProperties; | ||
variant: 'filled' | 'outline' | 'single'; | ||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void; | ||
icon?: typeof IconComponent; | ||
disabled?: boolean; | ||
title?: string; | ||
color: ThemeColor; | ||
} | ||
|
||
/** | ||
* A reusable input button component that follows the Button.tsx consistency | ||
* | ||
* @returns | ||
*/ | ||
export default function InputButton({ | ||
className, | ||
style, | ||
variant, | ||
onChange, | ||
icon, | ||
disabled, | ||
title, | ||
color, | ||
children, | ||
}: React.PropsWithChildren<Props>): JSX.Element { | ||
const Icon = icon; | ||
const isIconOnly = !children && !!icon; | ||
const colorHex = getThemeColorHexByName(color); | ||
const colorRgb = getThemeColorRgbByName(color)?.join(' '); | ||
|
||
return ( | ||
<label | ||
style={ | ||
{ | ||
...style, | ||
color: disabled ? 'ut-gray' : colorHex, | ||
backgroundColor: `rgb(${colorRgb} / var(--un-bg-opacity)`, | ||
} satisfies React.CSSProperties | ||
} | ||
className={clsx( | ||
'btn', | ||
{ | ||
'text-white! bg-opacity-100 hover:enabled:shadow-md active:enabled:shadow-sm shadow-black/20': | ||
variant === 'filled', | ||
'bg-opacity-0 border-current hover:enabled:bg-opacity-8 border': variant === 'outline', | ||
'bg-opacity-0 border-none hover:enabled:bg-opacity-8': variant === 'single', // settings is the only "single" | ||
'px-2 py-1.25': isIconOnly && variant !== 'outline', | ||
'px-1.75 py-1.25': isIconOnly && variant === 'outline', | ||
'px-3.75': variant === 'outline' && !isIconOnly, | ||
}, | ||
className | ||
)} | ||
title={title} | ||
> | ||
{Icon && <Icon className='h-6 w-6' />} | ||
{!isIconOnly && ( | ||
<Text variant='h4' className='inline-flex translate-y-0.08 items-center gap-2'> | ||
{children} | ||
</Text> | ||
)} | ||
<input type='file' className='hidden' disabled={disabled} onChange={disabled ? undefined : onChange} /> | ||
</label> | ||
); | ||
} |
Oops, something went wrong.