Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,35 @@
"es2021": true
},
"extends": "next/core-web-vitals",
"plugins": ["@typescript-eslint"],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"@typescript-eslint/no-explicit-any": [
"error",
{
"fixToUnknown": true
}
],
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"ignoreRestSiblings": true
}
],
"no-restricted-syntax": [
"error",
{
"selector": "TSAsExpression",
"message": "Do not use TypeScript type assertions; use declarations, generics, unions, or narrowing."
},
{
"selector": "TSTypeAssertion",
"message": "Do not use TypeScript type assertions; use declarations, generics, unions, or narrowing."
}
]
}
}
3 changes: 3 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
- name: Run lint
run: pnpm lint

- name: Reject authored JavaScript
run: pnpm check:javascript

- name: Run type check
run: pnpm type-check

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,7 @@ tsconfig.tsbuildinfo

# Claude Code local settings
.claude/

# Codex/agent-local skills
/.agents/
pnpm-lock.yaml
1 change: 0 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# Keep this minimal; Prettier already respects .gitignore
# Add only exceptions not covered by .gitignore if needed
pnpm-lock.yaml

9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
SHELL := /bin/sh
NPM ?= npm

.PHONY: help install dev build start lint format format-fix type-check check test docker-up docker-down docker-logs ci
.PHONY: help install dev build start lint format format-fix type-check check check-javascript test docker-up docker-down docker-logs ci

help:
@echo "Available targets:"
Expand All @@ -15,6 +15,7 @@ help:
@echo " format - prettier --check"
@echo " format-fix - prettier --write"
@echo " type-check - tsc --noEmit"
@echo " check-javascript - reject authored JavaScript/JSX"
@echo " check - lint + type-check + format + build"
@echo " docker-up - docker compose up -d"
@echo " docker-down - docker compose down"
Expand Down Expand Up @@ -45,7 +46,10 @@ format-fix:
type-check:
$(NPM) run type-check

check: format-fix lint type-check build
check-javascript:
$(NPM) run check:javascript

check: format-fix lint type-check check-javascript build

ci: check

Expand All @@ -57,4 +61,3 @@ docker-down:

docker-logs:
docker compose logs -f

30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ pnpm dev

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.

[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.

The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
The `pages/api` directory is mapped to `/api/*`. TypeScript files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.

This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.

Expand Down Expand Up @@ -101,7 +101,7 @@ Next only substitutes `NEXT_PUBLIC_*` values that exist at build time, so
`process.env.NEXT_PUBLIC_BASE_URL` survives in the compiled server output as a
real runtime lookup and each environment supplies its own value through Coolify.

This works only because the value is read server-side (`utils/auditLogger.js`,
This works only because the value is read server-side (`utils/auditLogger.ts`,
imported solely by `pages/api/*`). Reading it from client code would yield
`undefined` in the browser — see the comment in the Dockerfile before changing
this.
Expand Down Expand Up @@ -153,32 +153,32 @@ must not be committed or uploaded.

PAGES FOLDER: files requiring mods to API routes:

- batch > [id].jsx
- student > [id].jsx
- batches.jsx
- courses.jsx
- students.jsx
- users.jsx
- batch > [id].tsx
- student > [id].tsx
- batches.tsx
- courses.tsx
- students.tsx
- users.tsx

.env file:

- Adjust MySQL, BETTER_AUTH_URL, and BETTER_AUTH_RP_ID values

NOTE REGARDING BATCH ATTENDANCE DROPDOWN:

- This can be edited in /utils/tableHelper.js
- This can be edited in `/utils` TypeScript helpers.

## CSV download - related modifications

- Components to modify:
- Vercel
- /pages/users.jsx\*: name, accessor, htmlFor, id (input attribute)
- /pages/api/getuserdata.js (if necessary)
- /pages/users.tsx\*: name, accessor, htmlFor, id (input attribute)
- /pages/api/getuserdata.ts (if necessary)
- DreamHost
- [https://visionaid.dreamhosters.com/csv/staff.php](https://visionaid.dreamhosters.com/csv/staff.php): th tag list
- /csv/csvfunctions.php: fputcsv(), while($row = mysqli_fetch_assoc($result)), get_all_user_records()

\*users.jsx file (staff) example: database and UI column names:
\*users.tsx file (staff) example: database and UI column names:

id Id
email Email
Expand All @@ -203,7 +203,7 @@ action Action
## Errors

- Can't log out? If you get an error preventing you from logging out:
please modify the /components/Navbar.jsx file per comments
please modify the `/components/Navbar.tsx` file per comments
at the top of the file.

## Dreamhost-mtl folder (DB hosting)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import React, { forwardRef, useRef, useState, useEffect } from "react";

const AccessibleSelectCellEditor = forwardRef((props) => {
const AccessibleSelectCellEditor = forwardRef<
HTMLSelectElement,
{
onValueChange: (_value: string) => void;
value?: string | number;
values?: Array<string | number>;
stopEditing?: (_cancel?: boolean) => void;
colDef?: { headerName?: string; field?: string };
data?: { name?: string };
formatter?: (_value: string | number | undefined) => string;
}
>((props, _ref) => {
const { onValueChange, value, values, stopEditing, colDef, data, formatter } = props;

// If a formatter is provided, use it to format the initial value for display
const formattedValue = formatter ? formatter(value) : value;

const [selectedValue, setSelectedValue] = useState(formattedValue || "");
const selectRef = useRef(null);
const selectRef = useRef<HTMLSelectElement>(null);
const valueRef = useRef(formattedValue || "");

// Auto-focus and open the select element when the editor mounts
Expand All @@ -29,19 +40,19 @@ const AccessibleSelectCellEditor = forwardRef((props) => {
}
}, []);

const handleChange = (e) => {
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newValue = e.target.value;
valueRef.current = newValue;
setSelectedValue(newValue);

onValueChange(newValue);
stopEditing();
stopEditing?.();
};

const handleKeyDown = (e) => {
const handleKeyDown = (e: React.KeyboardEvent<HTMLSelectElement>) => {
if (e.key === "Escape") {
e.stopPropagation();
const resetValue = formatter ? formatter(value) : value;
const resetValue = formatter ? formatter(value) : (value ?? "");
valueRef.current = resetValue;
setSelectedValue(resetValue);
if (stopEditing) {
Expand Down
43 changes: 23 additions & 20 deletions components/Button.jsx → components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import Image from "next/image";
import PropTypes from "prop-types";
import styles from "@/styles/Button.module.css";
import type { CSSProperties, KeyboardEventHandler, MouseEventHandler } from "react";
import type { StaticImport } from "next/dist/shared/lib/get-img-props";

interface ButtonProps {
text?: string;
onClick?: MouseEventHandler<HTMLButtonElement>;
onKeyDown?: KeyboardEventHandler<HTMLButtonElement>;
iconSrc?: string | StaticImport;
iconAlt?: string;
isLight?: boolean;
style?: CSSProperties;
className?: string;
disabled?: boolean;
ariaControls?: string;
ariaExpanded?: boolean | "true" | "false";
ariaLabel?: string;
courseName?: string;
batch?: string | number;
tabIndex?: number;
"aria-label"?: string;
}

export default function Button({
text,
Expand All @@ -17,7 +37,7 @@ export default function Button({
ariaLabel, // if needed, explicitly specify
courseName, // if needed, pass only as data-attr
batch, // if needed, pass only as data-attr
}) {
}: ButtonProps) {
const buttonClassName = [isLight ? styles.genericButtonLight : styles.genericButtonDark, className]
.filter(Boolean)
.join(" ");
Expand All @@ -40,7 +60,7 @@ export default function Button({
aria-controls={ariaControls || undefined}
aria-label={computedAriaLabel}
data-course-name={courseName ?? undefined}
data-batch={batch ?? undefined}
data-batch={batch == null ? undefined : String(batch)}
>
{iconSrc ? (
<Image
Expand All @@ -56,20 +76,3 @@ export default function Button({
</button>
);
}

Button.propTypes = {
text: PropTypes.string,
onClick: PropTypes.func,
onKeyDown: PropTypes.func,
iconSrc: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), // next/image StaticImport allow (next/image StaticImport allow)
iconAlt: PropTypes.string,
isLight: PropTypes.bool,
style: PropTypes.object,
className: PropTypes.string,
disabled: PropTypes.bool,
ariaControls: PropTypes.string,
ariaExpanded: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
ariaLabel: PropTypes.string,
courseName: PropTypes.string,
batch: PropTypes.string,
};
File renamed without changes.
27 changes: 21 additions & 6 deletions components/DataGrid.jsx → components/DataGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { getRowStyle } from "@/utils/get-row-style";
import { smartComparator } from "@/utils/grid-comparators";
import type { CellKeyDownEvent, ColDef, GridReadyEvent, TabToNextCellParams } from "ag-grid-community";
import { AgGridReact } from "ag-grid-react";
import React from "react";

export function DataGrid({ title, rowData, onGridReady, onCellKeyDown, exitTargetId }) {
const columnDefs = [
type RowData = Record<string, unknown>;

interface DataGridProps {
title: string;
rowData: RowData[];
onGridReady?: (_event: GridReadyEvent<RowData>) => void;
onCellKeyDown?: (_event: CellKeyDownEvent<RowData>) => void;
exitTargetId: string;
}

export function DataGrid({ title, rowData, onGridReady, onCellKeyDown, exitTargetId }: DataGridProps) {
const columnDefs: ColDef<RowData>[] = [
{
field: "id",
headerName: "ID",
Expand Down Expand Up @@ -39,7 +50,7 @@ export function DataGrid({ title, rowData, onGridReady, onCellKeyDown, exitTarge
comparator: smartComparator,
};

const tabToNextCell = (params) => {
const tabToNextCell = (params: TabToNextCellParams<RowData>) => {
const { previousCellPosition, backwards, api } = params;

if (!previousCellPosition) return null;
Expand All @@ -59,13 +70,17 @@ export function DataGrid({ title, rowData, onGridReady, onCellKeyDown, exitTarge
return null;
}

const firstColumn = api.getAllDisplayedColumns()[0];
if (!firstColumn) return null;

return {
rowIndex: nextRowIndex,
column: api.getAllDisplayedColumns()[0],
rowPinned: null,
column: firstColumn,
};
};

const rowSelection = {
const rowSelection: { mode: "multiRow" } = {
mode: "multiRow",
};

Expand All @@ -83,7 +98,7 @@ export function DataGrid({ title, rowData, onGridReady, onCellKeyDown, exitTarge
Use arrow keys to navigate rows. Press space to select students.
</p>

<AgGridReact
<AgGridReact<RowData>
enableCellTextSelection={true}
rowData={rowData}
columnDefs={columnDefs}
Expand Down
24 changes: 18 additions & 6 deletions components/DropdownMenuPm.js → components/DropdownMenuPm.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
// Part of page group that populate a dynamic dropdown from MySQL:
// /db.js
// /pages/api/dropdownDataPm.js
// /pages/api/dropdownDataStaff.js
// /components/DropdownMenuStaff.js
// /lib/db.ts
// /pages/api/dropdownDataPm.ts
// /pages/api/dropdownDataStaff.ts
// /components/DropdownMenuStaff.tsx
// Author: Dante Ciolfi
// Last update: 5/4/2024

import React, { useState, useEffect } from "react";
import styles from "../styles/Home.module.css";

const DropdownMenuPm = ({ id = "PM", name = "PM", required = false, className = "", selectedValue = "" }) => {
const [options, setOptions] = useState([]);
const DropdownMenuPm = ({
id = "PM",
name = "PM",
required = false,
className = "",
selectedValue = "",
}: {
id?: string;
name?: string;
required?: boolean;
className?: string;
selectedValue?: string;
}) => {
const [options, setOptions] = useState<Array<{ id?: string | number; value?: string; name: string }>>([]);

useEffect(() => {
const fetchData = async () => {
Expand Down
Loading
Loading