feat: report issue popup (#261)
* feat: report issue popup * style: modified styles in feedback form * chore: minor UI fixes * chore: update useEffect * chore: change width to 400px --------- Co-authored-by: doprz <52579214+doprz@users.noreply.github.com> Co-authored-by: Isaiah David Rodriguez <51803892+IsaDavRod@users.noreply.github.com>
This commit is contained in:
@@ -26,6 +26,7 @@
|
|||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@sentry/react": "^8.33.1",
|
||||||
"@headlessui/react": "^2.1.9",
|
"@headlessui/react": "^2.1.9",
|
||||||
"@hello-pangea/dnd": "^17.0.0",
|
"@hello-pangea/dnd": "^17.0.0",
|
||||||
"@octokit/rest": "^21.0.2",
|
"@octokit/rest": "^21.0.2",
|
||||||
|
|||||||
2609
pnpm-lock.yaml
generated
2609
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,6 @@
|
|||||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||||
import { generateRandomId } from '@shared/util/random';
|
import { generateRandomId } from '@shared/util/random';
|
||||||
|
|
||||||
import handleDuplicate from './handleDuplicate';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new schedule with the given name
|
* Creates a new schedule with the given name
|
||||||
* @param scheduleName the name of the schedule to create
|
* @param scheduleName the name of the schedule to create
|
||||||
@@ -10,13 +8,23 @@ import handleDuplicate from './handleDuplicate';
|
|||||||
*/
|
*/
|
||||||
export default async function createSchedule(scheduleName: string): Promise<string | undefined> {
|
export default async function createSchedule(scheduleName: string): Promise<string | undefined> {
|
||||||
const schedules = await UserScheduleStore.get('schedules');
|
const schedules = await UserScheduleStore.get('schedules');
|
||||||
|
// get the number of schedules that either have the same name or have the same name with a number appended (e.g. "New Schedule (1)")
|
||||||
|
// this way we can prevent duplicate schedule names and increment the number if necessary
|
||||||
|
|
||||||
// Duplicate schedule found, we need to append a number to the end of the schedule name
|
// Regex to match schedule names that follow the pattern "ScheduleName" or "ScheduleName (1)", "ScheduleName (2)", etc.
|
||||||
const updatedName = await handleDuplicate(scheduleName);
|
const regex = new RegExp(`^${scheduleName}( \\(\\d+\\))?$`);
|
||||||
|
|
||||||
|
// Find how many schedules match the base name or follow the pattern with a number
|
||||||
|
const count = schedules.filter(s => regex.test(s.name)).length;
|
||||||
|
|
||||||
|
// If any matches are found, append the next number to the schedule name
|
||||||
|
let name = scheduleName;
|
||||||
|
if (count > 0) {
|
||||||
|
name = `${scheduleName} (${count})`;
|
||||||
|
}
|
||||||
schedules.push({
|
schedules.push({
|
||||||
id: generateRandomId(),
|
id: generateRandomId(),
|
||||||
name: updatedName,
|
name,
|
||||||
courses: [],
|
courses: [],
|
||||||
hours: 0,
|
hours: 0,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 360px;
|
width: 400px;
|
||||||
height: 540px;
|
height: 540px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
16
src/pages/report/index.html
Normal file
16
src/pages/report/index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<title>UTRP Report Issue</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
|
||||||
|
<script src="./index.tsx" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5
src/pages/report/index.tsx
Normal file
5
src/pages/report/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import ReportIssueMain from '@views/components/ReportIssueMain';
|
||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(<ReportIssueMain />);
|
||||||
28
src/shared/util/openReportWindow.ts
Normal file
28
src/shared/util/openReportWindow.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Open the report window relative centered to the current window.
|
||||||
|
*/
|
||||||
|
export function openReportWindow() {
|
||||||
|
chrome.windows.getCurrent({ populate: false }, currentWindow => {
|
||||||
|
const width = 400;
|
||||||
|
const height = 600;
|
||||||
|
|
||||||
|
// Calculate the new window's position to center it relative to the current window
|
||||||
|
const left =
|
||||||
|
currentWindow.left && currentWindow.width
|
||||||
|
? Math.round(currentWindow.left + (currentWindow.width - width) / 2)
|
||||||
|
: undefined;
|
||||||
|
const top =
|
||||||
|
currentWindow.top && currentWindow.height
|
||||||
|
? Math.round(currentWindow.top + (currentWindow.height - height) / 2)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
chrome.windows.create({
|
||||||
|
url: chrome.runtime.getURL(`report.html`),
|
||||||
|
type: 'popup',
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,22 +1,26 @@
|
|||||||
import splashText from '@assets/insideJokes';
|
import splashText from '@assets/insideJokes';
|
||||||
|
import createSchedule from '@pages/background/lib/createSchedule';
|
||||||
import { background } from '@shared/messages';
|
import { background } from '@shared/messages';
|
||||||
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
|
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
|
||||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||||
|
import { openReportWindow } from '@shared/util/openReportWindow';
|
||||||
import Divider from '@views/components/common/Divider';
|
import Divider from '@views/components/common/Divider';
|
||||||
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
|
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
|
||||||
import List from '@views/components/common/List';
|
import List from '@views/components/common/List';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
import useSchedules, { getActiveSchedule, replaceSchedule, switchSchedule } from '@views/hooks/useSchedules';
|
import useSchedules, { getActiveSchedule, replaceSchedule, switchSchedule } from '@views/hooks/useSchedules';
|
||||||
import { getUpdatedAtDateTimeString } from '@views/lib/getUpdatedAtDateTimeString';
|
import { getUpdatedAtDateTimeString } from '@views/lib/getUpdatedAtDateTimeString';
|
||||||
import { openTabFromContentScript } from '@views/lib/openNewTabFromContentScript';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import useKC_DABR_WASM from 'kc-dabr-wasm';
|
import useKC_DABR_WASM from 'kc-dabr-wasm';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import AddSchedule from '~icons/material-symbols/add';
|
||||||
import CalendarIcon from '~icons/material-symbols/calendar-month';
|
import CalendarIcon from '~icons/material-symbols/calendar-month';
|
||||||
|
import Feedback from '~icons/material-symbols/flag';
|
||||||
import RefreshIcon from '~icons/material-symbols/refresh';
|
import RefreshIcon from '~icons/material-symbols/refresh';
|
||||||
import SettingsIcon from '~icons/material-symbols/settings';
|
import SettingsIcon from '~icons/material-symbols/settings';
|
||||||
|
|
||||||
|
import { Button } from './common/Button';
|
||||||
import CourseStatus from './common/CourseStatus';
|
import CourseStatus from './common/CourseStatus';
|
||||||
import DialogProvider from './common/DialogProvider/DialogProvider';
|
import DialogProvider from './common/DialogProvider/DialogProvider';
|
||||||
import { SmallLogo } from './common/LogoIcon';
|
import { SmallLogo } from './common/LogoIcon';
|
||||||
@@ -34,10 +38,13 @@ export default function PopupMain(): JSX.Element {
|
|||||||
useKC_DABR_WASM();
|
useKC_DABR_WASM();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initSettings().then(({ enableCourseStatusChips, enableDataRefreshing }) => {
|
const initAllSettings = async () => {
|
||||||
|
const { enableCourseStatusChips, enableDataRefreshing } = await initSettings();
|
||||||
setEnableCourseStatusChips(enableCourseStatusChips);
|
setEnableCourseStatusChips(enableCourseStatusChips);
|
||||||
setEnableDataRefreshing(enableDataRefreshing);
|
setEnableDataRefreshing(enableDataRefreshing);
|
||||||
});
|
};
|
||||||
|
|
||||||
|
initAllSettings();
|
||||||
|
|
||||||
const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => {
|
const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => {
|
||||||
setEnableCourseStatusChips(newValue);
|
setEnableCourseStatusChips(newValue);
|
||||||
@@ -68,7 +75,7 @@ export default function PopupMain(): JSX.Element {
|
|||||||
|
|
||||||
const handleOpenOptions = async () => {
|
const handleOpenOptions = async () => {
|
||||||
const url = chrome.runtime.getURL('/options.html');
|
const url = chrome.runtime.getURL('/options.html');
|
||||||
await openTabFromContentScript(url);
|
background.openNewTab({ url });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCalendarOpenOnClick = async () => {
|
const handleCalendarOpenOnClick = async () => {
|
||||||
@@ -93,6 +100,9 @@ export default function PopupMain(): JSX.Element {
|
|||||||
<button className='bg-transparent px-2 py-1.25 btn' onClick={handleOpenOptions}>
|
<button className='bg-transparent px-2 py-1.25 btn' onClick={handleOpenOptions}>
|
||||||
<SettingsIcon className='size-6 color-ut-black' />
|
<SettingsIcon className='size-6 color-ut-black' />
|
||||||
</button>
|
</button>
|
||||||
|
<button className='bg-transparent px-2 py-1.25 btn' onClick={openReportWindow}>
|
||||||
|
<Feedback className='size-6 color-ut-black' />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,6 +132,16 @@ export default function PopupMain(): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
|
<div className='bottom-0 right-0 mt-2.5 w-full flex justify-end'>
|
||||||
|
<Button
|
||||||
|
variant='filled'
|
||||||
|
color='ut-burntorange'
|
||||||
|
className='h-fit p-0 btn'
|
||||||
|
onClick={() => createSchedule('New Schedule')}
|
||||||
|
>
|
||||||
|
<AddSchedule className='h-6 w-6' />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</ScheduleDropdown>
|
</ScheduleDropdown>
|
||||||
</div>
|
</div>
|
||||||
{activeSchedule?.courses?.length === 0 && (
|
{activeSchedule?.courses?.length === 0 && (
|
||||||
|
|||||||
127
src/views/components/ReportIssueMain.tsx
Normal file
127
src/views/components/ReportIssueMain.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import 'uno.css';
|
||||||
|
|
||||||
|
import { BrowserClient, captureFeedback, defaultStackParser, getCurrentScope, makeFetchTransport } from '@sentry/react';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { Button } from './common/Button';
|
||||||
|
import Text from './common/Text/Text';
|
||||||
|
|
||||||
|
const client = new BrowserClient({
|
||||||
|
dsn: 'https://ed1a50d8626ff6be35b98d7b1ec86d9d@o4508033820852224.ingest.us.sentry.io/4508033822490624',
|
||||||
|
integrations: [],
|
||||||
|
transport: makeFetchTransport,
|
||||||
|
stackParser: defaultStackParser,
|
||||||
|
});
|
||||||
|
|
||||||
|
getCurrentScope().setClient(client);
|
||||||
|
client.init();
|
||||||
|
|
||||||
|
const ReportIssueMain: React.FC = () => {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [feedback, setFeedback] = useState('');
|
||||||
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||||
|
|
||||||
|
const submitFeedback = async () => {
|
||||||
|
if (!email || !feedback) {
|
||||||
|
throw new Error('Email and feedback are required');
|
||||||
|
}
|
||||||
|
// Here you would typically send the feedback to a server
|
||||||
|
await captureFeedback(
|
||||||
|
{
|
||||||
|
message: feedback || 'No feedback provided',
|
||||||
|
email,
|
||||||
|
tags: {
|
||||||
|
version: chrome.runtime.getManifest().version,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
includeReplay: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset form fields and close the dialog
|
||||||
|
setEmail('');
|
||||||
|
setFeedback('');
|
||||||
|
setIsSubmitted(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isSubmitted) {
|
||||||
|
return (
|
||||||
|
<div className='w-80 flex flex-col rounded-lg bg-white p-6 shadow-lg'>
|
||||||
|
<Text variant='h2' className='mb-4'>
|
||||||
|
Thank you
|
||||||
|
</Text>
|
||||||
|
<Text variant='p' className='mb-6'>
|
||||||
|
Your feedback has been submitted. You may close this window.
|
||||||
|
</Text>
|
||||||
|
<Button variant='filled' color='ut-green' className='border-0' onClick={() => window.close()}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSubmitted) {
|
||||||
|
return (
|
||||||
|
<div className='w-80 bg-white p-6'>
|
||||||
|
<h2 className='mb-4 text-2xl text-orange font-bold'>{`Hook'em Horns!`}</h2>
|
||||||
|
<p className='mb-6 text-gray-600'>Your feedback is music to our ears. Thanks for helping us improve!</p>
|
||||||
|
<button
|
||||||
|
className='w-full rounded bg-orange-600 px-4 py-2 text-white font-bold transition duration-300 hover:bg-orange-700'
|
||||||
|
onClick={() => window.close()}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-80 bg-white p-6'>
|
||||||
|
<h2 className='mb-4 text-2xl text-ut-burntorange font-bold'>Longhorn Feedback</h2>
|
||||||
|
<p className='mb-4 text-sm text-ut-black'>Help us make UT Registration Plus even better!</p>
|
||||||
|
|
||||||
|
<form onSubmit={submitFeedback}>
|
||||||
|
<div className='mb-4'>
|
||||||
|
<label htmlFor='email' className='mb-1 block text-sm text-ut-black font-medium'>
|
||||||
|
Your @utexas.edu email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='email'
|
||||||
|
id='email'
|
||||||
|
value={email}
|
||||||
|
onChange={e => setEmail(e.target.value)}
|
||||||
|
className='w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-500'
|
||||||
|
placeholder='bevo@utexas.edu'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-4'>
|
||||||
|
<label htmlFor='feedback' className='mb-1 block text-sm text-ut-black font-medium'>
|
||||||
|
Your feedback
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id='feedback'
|
||||||
|
value={feedback}
|
||||||
|
onChange={e => setFeedback(e.target.value)}
|
||||||
|
className='h-24 w-full resize-none border border-gray-300 rounded-md px-3 py-2 text-sm font-sans focus:outline-none focus:ring-2 focus:ring-orange-500'
|
||||||
|
placeholder='I wish UT Registration Plus could...'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={submitFeedback}
|
||||||
|
variant='filled'
|
||||||
|
color='ut-orange'
|
||||||
|
className='w-full border-0 rounded bg-orange px-4 py-2 text-white font-bold transition duration-300 hover:bg-orange-700'
|
||||||
|
>
|
||||||
|
Send Feedback
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReportIssueMain;
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
|
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
|
||||||
import { Status } from '@shared/types/Course';
|
|
||||||
import { Button } from '@views/components/common/Button';
|
import { Button } from '@views/components/common/Button';
|
||||||
import CourseStatus from '@views/components/common/CourseStatus';
|
import CourseStatus from '@views/components/common/CourseStatus';
|
||||||
import Divider from '@views/components/common/Divider';
|
import Divider from '@views/components/common/Divider';
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ let initialLoad = true;
|
|||||||
const errorSchedule = new UserSchedule({
|
const errorSchedule = new UserSchedule({
|
||||||
courses: [],
|
courses: [],
|
||||||
id: 'error',
|
id: 'error',
|
||||||
name: 'An error has occurred',
|
name: 'No Schedule Selected',
|
||||||
hours: 0,
|
hours: 0,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const SiteSupport = {
|
|||||||
WAITLIST: 'WAITLIST',
|
WAITLIST: 'WAITLIST',
|
||||||
EXTENSION_POPUP: 'EXTENSION_POPUP',
|
EXTENSION_POPUP: 'EXTENSION_POPUP',
|
||||||
MY_CALENDAR: 'MY_CALENDAR',
|
MY_CALENDAR: 'MY_CALENDAR',
|
||||||
|
REPORT_ISSUE: 'REPORT_ISSUE',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ export default defineConfig({
|
|||||||
renameFile('src/pages/debug/index.html', 'debug.html'),
|
renameFile('src/pages/debug/index.html', 'debug.html'),
|
||||||
renameFile('src/pages/options/index.html', 'options.html'),
|
renameFile('src/pages/options/index.html', 'options.html'),
|
||||||
renameFile('src/pages/calendar/index.html', 'calendar.html'),
|
renameFile('src/pages/calendar/index.html', 'calendar.html'),
|
||||||
|
renameFile('src/pages/report/index.html', 'report.html'),
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
@@ -171,6 +172,10 @@ export default defineConfig({
|
|||||||
target: 'http://localhost:5173',
|
target: 'http://localhost:5173',
|
||||||
rewrite: path => path.replace('options', 'src/pages/options/index'),
|
rewrite: path => path.replace('options', 'src/pages/options/index'),
|
||||||
},
|
},
|
||||||
|
'/report.html': {
|
||||||
|
target: 'http://localhost:5173',
|
||||||
|
rewrite: path => path.replace('report', 'src/pages/report/index'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
@@ -179,6 +184,7 @@ export default defineConfig({
|
|||||||
debug: 'src/pages/debug/index.html',
|
debug: 'src/pages/debug/index.html',
|
||||||
calendar: 'src/pages/calendar/index.html',
|
calendar: 'src/pages/calendar/index.html',
|
||||||
options: 'src/pages/options/index.html',
|
options: 'src/pages/options/index.html',
|
||||||
|
report: 'src/pages/report/index.html',
|
||||||
},
|
},
|
||||||
// output: {
|
// output: {
|
||||||
// entryFileNames: `[name].js`, // otherwise it will add the hash
|
// entryFileNames: `[name].js`, // otherwise it will add the hash
|
||||||
|
|||||||
Reference in New Issue
Block a user