mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-23 10:02:51 +00:00
feat: type-safe sc-icons
This commit is contained in:
@@ -50,11 +50,11 @@ the config file.
|
|||||||
Icon font minification is done automatically, but requires you to
|
Icon font minification is done automatically, but requires you to
|
||||||
follow a few simple rules:
|
follow a few simple rules:
|
||||||
|
|
||||||
1. Use the tagged template literal for referencing icon names in
|
1. Use the type-safe proxy for referencing icon names in
|
||||||
TypeScript files and code
|
TypeScript files and code
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
SCIcon`icon_name`;
|
SCIcon.icon_name;
|
||||||
```
|
```
|
||||||
|
|
||||||
2. When using `ion-icon` in HTML, reference either icons that went through
|
2. When using `ion-icon` in HTML, reference either icons that went through
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import type {IconConfig} from './scripts/icon-config';
|
|
||||||
|
|
||||||
const config: IconConfig = {
|
/** @type {import('./scripts/icon-config').IconConfig} */
|
||||||
|
const config = {
|
||||||
inputPath: 'node_modules/material-symbols/material-symbols-rounded.woff2',
|
inputPath: 'node_modules/material-symbols/material-symbols-rounded.woff2',
|
||||||
outputPath: 'src/assets/icons.min.woff2',
|
outputPath: 'src/assets/icons.min.woff2',
|
||||||
htmlGlob: 'src/**/*.html',
|
htmlGlob: 'src/**/*.html',
|
||||||
@@ -38,10 +38,6 @@ const config: IconConfig = {
|
|||||||
'work',
|
'work',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
codePoints: {
|
|
||||||
ios_share: 'e6b8',
|
|
||||||
fact_check: 'f0c5',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"build:prod": "ng build --configuration=production",
|
"build:prod": "ng build --configuration=production",
|
||||||
"build:stats": "ng build --configuration=production --stats-json",
|
"build:stats": "ng build --configuration=production --stats-json",
|
||||||
"changelog": "conventional-changelog -p angular -i src/assets/about/CHANGELOG.md -s -r 0",
|
"changelog": "conventional-changelog -p angular -i src/assets/about/CHANGELOG.md -s -r 0",
|
||||||
"check-icons": "ts-node scripts/check-icon-correctness.ts",
|
"check-icons": "node scripts/check-icon-correctness.mjs",
|
||||||
"chromium:no-cors": "chromium --disable-web-security --user-data-dir=\".browser-data/chromium\"",
|
"chromium:no-cors": "chromium --disable-web-security --user-data-dir=\".browser-data/chromium\"",
|
||||||
"chromium:virtual-host": "chromium --host-resolver-rules=\"MAP mobile.app.uni-frankfurt.de:* localhost:8100\" --ignore-certificate-errors",
|
"chromium:virtual-host": "chromium --host-resolver-rules=\"MAP mobile.app.uni-frankfurt.de:* localhost:8100\" --ignore-certificate-errors",
|
||||||
"cypress:open": "cypress open",
|
"cypress:open": "cypress open",
|
||||||
@@ -35,10 +35,16 @@
|
|||||||
"e2e": "ng e2e",
|
"e2e": "ng e2e",
|
||||||
"format": "prettier . -c",
|
"format": "prettier . -c",
|
||||||
"format:fix": "prettier --write .",
|
"format:fix": "prettier --write .",
|
||||||
"licenses": "license-checker --json > src/assets/about/licenses.json && ts-node ./scripts/accumulate-licenses.ts && git add src/assets/about/licenses.json",
|
"licenses": "license-checker --json > src/assets/about/licenses.json && node ./scripts/accumulate-licenses.mjs && git add src/assets/about/licenses.json",
|
||||||
"lint": "ng lint && stylelint \"**/*.scss\"",
|
"lint": "ng lint && stylelint \"**/*.scss\"",
|
||||||
"lint:fix": "eslint --fix -c .eslintrc.json --ignore-path .eslintignore --ext .ts,.html src/ && stylelint --fix \"**/*.scss\"",
|
"lint:fix": "eslint --fix -c .eslintrc.json --ignore-path .eslintignore --ext .ts,.html src/ && stylelint --fix \"**/*.scss\"",
|
||||||
|
<<<<<<< HEAD
|
||||||
"minify-icons": "ts-node-esm scripts/minify-icon-font.ts",
|
"minify-icons": "ts-node-esm scripts/minify-icon-font.ts",
|
||||||
|
||||||| parent of 7b431b8f (feat: type-safe sc-icons)
|
||||||
|
"minify-icons": "ts-node scripts/minify-icon-font.ts",
|
||||||
|
=======
|
||||||
|
"minify-icons": "node scripts/minify-icon-font.mjs",
|
||||||
|
>>>>>>> 7b431b8f (feat: type-safe sc-icons)
|
||||||
"postinstall": "jetify && echo \"skipping jetify in production mode\"",
|
"postinstall": "jetify && echo \"skipping jetify in production mode\"",
|
||||||
"preview": "http-server www --p 8101 -o",
|
"preview": "http-server www --p 8101 -o",
|
||||||
"push": "git push && git push origin \"v$npm_package_version\"",
|
"push": "git push && git push origin \"v$npm_package_version\"",
|
||||||
|
|||||||
@@ -12,33 +12,34 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import fs from 'fs';
|
import {readFileSync, writeFileSync} from 'fs';
|
||||||
import {omit} from '../src/app/_helpers/collections/omit';
|
import {omit, pickBy} from '@openstapps/collection-utils';
|
||||||
import {pickBy} from '../src/app/_helpers/collections/pick';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* accumulate and transform licenses based on two license files
|
* accumulate and transform licenses based on two license files
|
||||||
|
* @param {string} path
|
||||||
|
* @param {string} additionalLicensesPath
|
||||||
*/
|
*/
|
||||||
function accumulateFile(path: string, additionalLicensesPath: string) {
|
function accumulateFile(path, additionalLicensesPath) {
|
||||||
const packageJson = JSON.parse(fs.readFileSync('./package.json').toString());
|
const packageJson = JSON.parse(readFileSync('./package.json').toString());
|
||||||
const dependencies = packageJson.dependencies;
|
const dependencies = packageJson.dependencies;
|
||||||
|
|
||||||
console.log(`Accumulating licenses from ${path}`);
|
console.log(`Accumulating licenses from ${path}`);
|
||||||
|
|
||||||
fs.writeFileSync(
|
writeFileSync(
|
||||||
path,
|
path,
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
Object.entries<any>({
|
Object.entries({
|
||||||
...pickBy(JSON.parse(fs.readFileSync(path).toString()), (_, key: string) => {
|
...pickBy(JSON.parse(readFileSync(path).toString()), (_, key) => {
|
||||||
const parts = key.split('@');
|
const parts = /** @type {string} */ (key).split('@');
|
||||||
|
|
||||||
return dependencies[parts.slice(0, -1).join('@')] === parts[parts.length - 1];
|
return dependencies[parts.slice(0, -1).join('@')] === parts[parts.length - 1];
|
||||||
}),
|
}),
|
||||||
...JSON.parse(fs.readFileSync(additionalLicensesPath).toString()),
|
...JSON.parse(readFileSync(additionalLicensesPath).toString()),
|
||||||
})
|
})
|
||||||
.map(([key, value]) => ({
|
.map(([key, value]) => ({
|
||||||
licenseText: value.licenseFile && fs.readFileSync(value.licenseFile, 'utf8'),
|
licenseText: value.licenseFile && readFileSync(value.licenseFile, 'utf8'),
|
||||||
name: key,
|
name: key,
|
||||||
...omit(value, 'licenseFile', 'path'),
|
...omit(value, 'licenseFile', 'path'),
|
||||||
}))
|
}))
|
||||||
@@ -12,18 +12,20 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import fontkit, {Font} from 'fontkit';
|
import {openSync} from 'fontkit';
|
||||||
import config from '../icons.config';
|
import config from '../icons.config.mjs';
|
||||||
import {existsSync} from 'fs';
|
import {existsSync} from 'fs';
|
||||||
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons';
|
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons.mjs';
|
||||||
|
import {fetchCodePointMap} from './get-code-points.mjs';
|
||||||
|
|
||||||
const commandName = '"npm run minify-icons"';
|
const commandName = '"npm run minify-icons"';
|
||||||
const originalFont = fontkit.openSync(config.inputPath);
|
|
||||||
if (!existsSync(config.outputPath)) {
|
if (!existsSync(config.outputPath)) {
|
||||||
console.error(`Minified font not found. Run ${commandName} first.`);
|
console.error(`Minified font not found. Run ${commandName} first.`);
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
const modifiedFont = fontkit.openSync(config.outputPath);
|
|
||||||
|
/** @type {import('fontkit').Font} */
|
||||||
|
const modifiedFont = openSync(config.outputPath);
|
||||||
|
|
||||||
let success = true;
|
let success = true;
|
||||||
|
|
||||||
@@ -48,25 +50,16 @@ async function checkAll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* @param {Record<string, string[]>} icons
|
||||||
*/
|
*/
|
||||||
function check(icons: Record<string, string[]>) {
|
async function check(icons) {
|
||||||
for (const [purpose, iconSet] of Object.entries(icons)) {
|
const codePoints = await fetchCodePointMap();
|
||||||
for (const icon of iconSet) {
|
|
||||||
if (!hasIcon(originalFont, icon)) {
|
|
||||||
success = false;
|
|
||||||
console.error(`${purpose}: ${icon} does not exist. Typo?`);
|
|
||||||
} else if (!hasIcon(modifiedFont, icon)) {
|
|
||||||
success = false;
|
|
||||||
console.error(`${purpose}: ${icon} not found in minified font. Run ${commandName} to regenerate it.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
for (const icon of Object.values(icons).flat()) {
|
||||||
*
|
const codePoint = codePoints.get(icon);
|
||||||
*/
|
if (!codePoint) throw new Error(`"${icon}" is not a valid icon`);
|
||||||
function hasIcon(font: Font, icon: string) {
|
if (!modifiedFont.getGlyph(Number.parseInt(codePoint, 16))) {
|
||||||
return font.layout(icon).glyphs.some(it => it.isLigature);
|
throw new Error(`"${icon}" (code point ${codePoint}) is missing`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -14,34 +14,39 @@
|
|||||||
*/
|
*/
|
||||||
import {glob} from 'glob';
|
import {glob} from 'glob';
|
||||||
import {readFileSync} from 'fs';
|
import {readFileSync} from 'fs';
|
||||||
import {matchPropertyContent, matchTagProperties} from '../src/app/util/ion-icon/icon-match';
|
import {
|
||||||
|
matchPropertyAccess,
|
||||||
|
matchPropertyContent,
|
||||||
|
matchTagProperties,
|
||||||
|
} from '../src/app/util/ion-icon/icon-match.mjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* @returns {Promise<Record<string, string[]>>}
|
||||||
*/
|
*/
|
||||||
export async function getUsedIconsHtml(pattern = 'src/**/*.html'): Promise<Record<string, string[]>> {
|
export async function getUsedIconsHtml(pattern = 'src/**/*.html') {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
(await glob(pattern))
|
(await glob(pattern))
|
||||||
.map(file => [
|
.map(file => [
|
||||||
file,
|
file,
|
||||||
(readFileSync(file, 'utf8')
|
readFileSync(file, 'utf8')
|
||||||
.match(matchTagProperties('ion-icon'))
|
.match(matchTagProperties('ion-icon'))
|
||||||
?.flatMap(match => {
|
?.flatMap(match => {
|
||||||
return match.match(matchPropertyContent(['name', 'md', 'ios']));
|
return match.match(matchPropertyContent(['name', 'md', 'ios']));
|
||||||
})
|
})
|
||||||
.filter(it => !!it) as string[]) || [],
|
.filter(it => !!it) || [],
|
||||||
])
|
])
|
||||||
.filter(([, values]) => values.length > 0),
|
.filter(([, values]) => values && values.length > 0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* @returns {Promise<Record<string, string[]>>}
|
||||||
*/
|
*/
|
||||||
export async function getUsedIconsTS(pattern = 'src/**/*.ts'): Promise<Record<string, string[]>> {
|
export async function getUsedIconsTS(pattern = 'src/**/*.ts') {
|
||||||
|
const regex = matchPropertyAccess('SCIcon');
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
(await glob(pattern))
|
(await glob(pattern))
|
||||||
.map(file => [file, readFileSync(file, 'utf8').match(/(?<=Icon`)[\w-]+(?=`)/g) || []])
|
.map(file => [file, readFileSync(file, 'utf8').match(regex) || []])
|
||||||
.filter(([, values]) => values.length > 0),
|
.filter(([, values]) => values && values.length > 0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
23
frontend/app/scripts/get-code-points.mjs
Normal file
23
frontend/app/scripts/get-code-points.mjs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const url =
|
||||||
|
'https://raw.githubusercontent.com/google/material-design-icons/master/' +
|
||||||
|
'variablefont/MaterialSymbolsRounded%5BFILL%2CGRAD%2Copsz%2Cwght%5D.codepoints';
|
||||||
|
|
||||||
|
export async function fetchCodePointMap() {
|
||||||
|
const icons = await fetch(url)
|
||||||
|
.then(it => it.text())
|
||||||
|
.then(it => new Map(it.split('\n').map(it => /** @type {[string, string]} */ (it.split(' ')))));
|
||||||
|
if (icons.size < 100) throw new Error(`Code point map is very small, is the URL incorrect? ${url}`);
|
||||||
|
return icons;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} icons
|
||||||
|
*/
|
||||||
|
export async function getCodePoints(icons) {
|
||||||
|
const codePoints = await fetchCodePointMap();
|
||||||
|
return icons.map(icon => {
|
||||||
|
const code = codePoints.get(icon);
|
||||||
|
if (!code) throw new Error(`Code point for icon ${icon} not found`);
|
||||||
|
return code;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -19,5 +19,4 @@ export interface IconConfig {
|
|||||||
inputPath: string;
|
inputPath: string;
|
||||||
outputPath: string;
|
outputPath: string;
|
||||||
additionalIcons?: {[purpose: string]: string[]};
|
additionalIcons?: {[purpose: string]: string[]};
|
||||||
codePoints?: {[name: string]: string};
|
|
||||||
}
|
}
|
||||||
@@ -14,16 +14,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import fontkit from 'fontkit';
|
|
||||||
import {exec} from 'child_process';
|
import {exec} from 'child_process';
|
||||||
import config from '../icons.config';
|
import config from '../icons.config.mjs';
|
||||||
import {statSync} from 'fs';
|
import {statSync} from 'fs';
|
||||||
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons';
|
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons.mjs';
|
||||||
|
import {getCodePoints} from './get-code-points.mjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* @param {string[] | string} command
|
||||||
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
async function run(command: string[] | string): Promise<string> {
|
async function run(command) {
|
||||||
const fullCommand = Array.isArray(command) ? command.join(' ') : command;
|
const fullCommand = Array.isArray(command) ? command.join(' ') : command;
|
||||||
console.log(`>> ${fullCommand}`);
|
console.log(`>> ${fullCommand}`);
|
||||||
|
|
||||||
@@ -44,7 +45,8 @@ async function run(command: string[] | string): Promise<string> {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async function minifyIconFont() {
|
async function minifyIconFont() {
|
||||||
const icons = new Set<string>();
|
/** @type {Set<string>} */
|
||||||
|
const icons = new Set();
|
||||||
|
|
||||||
for (const iconSet of [
|
for (const iconSet of [
|
||||||
...Object.values(config.additionalIcons || []),
|
...Object.values(config.additionalIcons || []),
|
||||||
@@ -56,35 +58,7 @@ async function minifyIconFont() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Icons used:', [...icons.values()].sort());
|
const glyphs = ['5f-7a', '30-39', ...(await getCodePoints([...icons]))].sort();
|
||||||
const font = fontkit.openSync(config.inputPath);
|
|
||||||
|
|
||||||
const glyphs: string[] = ['5f-7a', '30-39'];
|
|
||||||
for (const icon of icons) {
|
|
||||||
const iconGlyphs = font.layout(icon).glyphs;
|
|
||||||
if (iconGlyphs.length === 0) {
|
|
||||||
console.error(`${icon} not found in font. Typo?`);
|
|
||||||
process.exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const codePoints = iconGlyphs
|
|
||||||
.flatMap(it => font.stringsForGlyph(it.id))
|
|
||||||
.flatMap(it => [...it])
|
|
||||||
.map(it => it.codePointAt(0)!.toString(16));
|
|
||||||
|
|
||||||
if (codePoints.length === 0) {
|
|
||||||
if (config.codePoints?.[icon]) {
|
|
||||||
glyphs.push(config.codePoints[icon]);
|
|
||||||
} else {
|
|
||||||
console.log();
|
|
||||||
console.error(`${icon} code point could not be determined. Add it to config.codePoints.`);
|
|
||||||
process.exit(-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
glyphs.push(...codePoints);
|
|
||||||
}
|
|
||||||
glyphs.sort();
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
await run([
|
await run([
|
||||||
@@ -114,8 +88,10 @@ minifyIconFont();
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Bytes to respective units
|
* Bytes to respective units
|
||||||
|
* @param {number} value
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function toByteUnit(value: number): string {
|
function toByteUnit(value) {
|
||||||
if (value < 1024) {
|
if (value < 1024) {
|
||||||
return `${value}B`;
|
return `${value}B`;
|
||||||
} else if (value < 1024 * 1024) {
|
} else if (value < 1024 * 1024) {
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
@if (content.type === 'router link') {
|
@if (content.type === 'router link') {
|
||||||
<ion-item [routerLink]="content.link">
|
<ion-item [routerLink]="content.link">
|
||||||
@if (content.icon) {
|
@if (content.icon) {
|
||||||
<ion-icon [name]="content.icon" slot="start"></ion-icon>
|
<ion-icon [name]="$any(content.icon)" slot="start"></ion-icon>
|
||||||
}
|
}
|
||||||
<ion-label>{{ 'title' | translateSimple: content }}</ion-label>
|
<ion-label>{{ 'title' | translateSimple: content }}</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ describe('ConfigProvider', () => {
|
|||||||
storageProviderSpy.has.and.returnValue(Promise.resolve(true));
|
storageProviderSpy.has.and.returnValue(Promise.resolve(true));
|
||||||
const wrongConfig = structuredClone(sampleIndexResponse);
|
const wrongConfig = structuredClone(sampleIndexResponse);
|
||||||
wrongConfig.backend.SCVersion = '0.1.0';
|
wrongConfig.backend.SCVersion = '0.1.0';
|
||||||
storageProviderSpy.get.and.returnValue(wrongConfig);
|
storageProviderSpy.get.and.returnValue(Promise.resolve(wrongConfig));
|
||||||
spyOn(configProvider.client, 'handshake').and.returnValue(Promise.resolve(sampleIndexResponse));
|
spyOn(configProvider.client, 'handshake').and.returnValue(Promise.resolve(sampleIndexResponse));
|
||||||
await configProvider.init();
|
await configProvider.init();
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {AddEventStates, AddEventStatesMap} from './add-event-action-chip.config'
|
|||||||
import {EditEventSelectionComponent} from '../edit-event-selection.component';
|
import {EditEventSelectionComponent} from '../edit-event-selection.component';
|
||||||
import {AddEventReviewModalComponent} from '../../../calendar/add-event-review-modal.component';
|
import {AddEventReviewModalComponent} from '../../../calendar/add-event-review-modal.component';
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||||
|
import {MaterialSymbol} from 'material-symbols';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows a horizontal list of action chips
|
* Shows a horizontal list of action chips
|
||||||
@@ -55,7 +56,7 @@ export class AddEventActionChipComponent {
|
|||||||
/**
|
/**
|
||||||
* Icon
|
* Icon
|
||||||
*/
|
*/
|
||||||
icon: string;
|
icon: MaterialSymbol;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current state of icon fill
|
* Current state of icon fill
|
||||||
|
|||||||
@@ -24,28 +24,28 @@ export enum AddEventStates {
|
|||||||
|
|
||||||
export const AddEventStatesMap = {
|
export const AddEventStatesMap = {
|
||||||
[AddEventStates.ADDED_ALL]: {
|
[AddEventStates.ADDED_ALL]: {
|
||||||
icon: SCIcon`event_available`,
|
icon: SCIcon.event_available,
|
||||||
fill: true,
|
fill: true,
|
||||||
label: 'data.chips.add_events.ADDED_ALL',
|
label: 'data.chips.add_events.ADDED_ALL',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
color: 'success',
|
color: 'success',
|
||||||
},
|
},
|
||||||
[AddEventStates.ADDED_SOME]: {
|
[AddEventStates.ADDED_SOME]: {
|
||||||
icon: SCIcon`event`,
|
icon: SCIcon.event,
|
||||||
fill: true,
|
fill: true,
|
||||||
label: 'data.chips.add_events.ADDED_SOME',
|
label: 'data.chips.add_events.ADDED_SOME',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
color: 'success',
|
color: 'success',
|
||||||
},
|
},
|
||||||
[AddEventStates.REMOVED_ALL]: {
|
[AddEventStates.REMOVED_ALL]: {
|
||||||
icon: SCIcon`calendar_today`,
|
icon: SCIcon.calendar_today,
|
||||||
fill: false,
|
fill: false,
|
||||||
label: 'data.chips.add_events.REMOVED_ALL',
|
label: 'data.chips.add_events.REMOVED_ALL',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
},
|
},
|
||||||
[AddEventStates.UNAVAILABLE]: {
|
[AddEventStates.UNAVAILABLE]: {
|
||||||
icon: SCIcon`event_busy`,
|
icon: SCIcon.event_busy,
|
||||||
fill: false,
|
fill: false,
|
||||||
label: 'data.chips.add_events.UNAVAILABLE',
|
label: 'data.chips.add_events.UNAVAILABLE',
|
||||||
disabled: true,
|
disabled: true,
|
||||||
|
|||||||
@@ -14,37 +14,38 @@
|
|||||||
*/
|
*/
|
||||||
import {SCThingType} from '@openstapps/core';
|
import {SCThingType} from '@openstapps/core';
|
||||||
import {SCIcon} from '../../util/ion-icon/icon';
|
import {SCIcon} from '../../util/ion-icon/icon';
|
||||||
|
import {MaterialSymbol} from 'material-symbols';
|
||||||
|
|
||||||
export const DataIcons: Record<SCThingType, string> = {
|
export const DataIcons = {
|
||||||
'academic event': SCIcon`school`,
|
'academic event': SCIcon.school,
|
||||||
'assessment': SCIcon`fact_check`,
|
'assessment': SCIcon.fact_check,
|
||||||
'article': SCIcon`article`,
|
'article': SCIcon.article,
|
||||||
'book': SCIcon`book`,
|
'book': SCIcon.book,
|
||||||
'building': SCIcon`location_city`,
|
'building': SCIcon.location_city,
|
||||||
'certification': SCIcon`contract`,
|
'certification': SCIcon.contract,
|
||||||
'catalog': SCIcon`inventory_2`,
|
'catalog': SCIcon.inventory_2,
|
||||||
'contact point': SCIcon`contact_page`,
|
'contact point': SCIcon.contact_page,
|
||||||
'course of study': SCIcon`school`,
|
'course of study': SCIcon.school,
|
||||||
'date series': SCIcon`event`,
|
'date series': SCIcon.event,
|
||||||
'dish': SCIcon`lunch_dining`,
|
'dish': SCIcon.lunch_dining,
|
||||||
'favorite': SCIcon`favorite`,
|
'favorite': SCIcon.favorite,
|
||||||
'floor': SCIcon`foundation`,
|
'floor': SCIcon.foundation,
|
||||||
'id card': SCIcon`badge`,
|
'id card': SCIcon.badge,
|
||||||
'message': SCIcon`newspaper`,
|
'message': SCIcon.newspaper,
|
||||||
'organization': SCIcon`business_center`,
|
'organization': SCIcon.business_center,
|
||||||
'periodical': SCIcon`feed`,
|
'periodical': SCIcon.feed,
|
||||||
'person': SCIcon`person`,
|
'person': SCIcon.person,
|
||||||
'point of interest': SCIcon`pin_drop`,
|
'point of interest': SCIcon.pin_drop,
|
||||||
'publication event': SCIcon`campaign`,
|
'publication event': SCIcon.campaign,
|
||||||
'room': SCIcon`meeting_room`,
|
'room': SCIcon.meeting_room,
|
||||||
'semester': SCIcon`date_range`,
|
'semester': SCIcon.date_range,
|
||||||
'setting': SCIcon`settings`,
|
'setting': SCIcon.settings,
|
||||||
'sport course': SCIcon`sports_soccer`,
|
'sport course': SCIcon.sports_soccer,
|
||||||
'study module': SCIcon`view_module`,
|
'study module': SCIcon.view_module,
|
||||||
'ticket': SCIcon`confirmation_number`,
|
'ticket': SCIcon.confirmation_number,
|
||||||
'todo': SCIcon`task`,
|
'todo': SCIcon.task,
|
||||||
'tour': SCIcon`tour`,
|
'tour': SCIcon.tour,
|
||||||
'video': SCIcon`movie`,
|
'video': SCIcon.movie,
|
||||||
'diff': SCIcon`difference`,
|
'diff': SCIcon.difference,
|
||||||
'job posting': SCIcon`work`,
|
'job posting': SCIcon.work,
|
||||||
};
|
} satisfies Record<SCThingType, MaterialSymbol>;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export class DataIconPipe implements PipeTransform {
|
|||||||
/**
|
/**
|
||||||
* Provide the icon name from the data type
|
* Provide the icon name from the data type
|
||||||
*/
|
*/
|
||||||
transform(type: SCThingType): string {
|
transform(type: SCThingType) {
|
||||||
return this.typeIconMap[type];
|
return this.typeIconMap[type];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import {SCThings} from '@openstapps/core';
|
|||||||
import {SCIcon} from '../../../util/ion-icon/icon';
|
import {SCIcon} from '../../../util/ion-icon/icon';
|
||||||
|
|
||||||
const AccordionButtonState = {
|
const AccordionButtonState = {
|
||||||
collapsed: SCIcon`expand_more`,
|
collapsed: SCIcon.expand_more,
|
||||||
expanded: SCIcon`expand_less`,
|
expanded: SCIcon.expand_less,
|
||||||
};
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -35,7 +35,8 @@ export class TitleCardComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
@ViewChild('accordionTextArea') accordionTextArea: ElementRef;
|
@ViewChild('accordionTextArea') accordionTextArea: ElementRef;
|
||||||
|
|
||||||
buttonState = AccordionButtonState.collapsed;
|
buttonState: (typeof AccordionButtonState)[keyof typeof AccordionButtonState] =
|
||||||
|
AccordionButtonState.collapsed;
|
||||||
|
|
||||||
buttonShown = true;
|
buttonShown = true;
|
||||||
|
|
||||||
|
|||||||
@@ -35,13 +35,13 @@
|
|||||||
lines="none"
|
lines="none"
|
||||||
class="menu-category"
|
class="menu-category"
|
||||||
>
|
>
|
||||||
<ion-icon slot="end" [name]="category.icon"></ion-icon>
|
<ion-icon slot="end" [name]="$any(category.icon)"></ion-icon>
|
||||||
<ion-label> {{ category.translations[language]?.title | titlecase }} </ion-label>
|
<ion-label> {{ category.translations[language]?.title | titlecase }} </ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
}
|
}
|
||||||
@for (item of category.items; track item) {
|
@for (item of category.items; track item) {
|
||||||
<ion-item [rootLink]="item.route" [redirectedFrom]="item.route">
|
<ion-item [rootLink]="item.route" [redirectedFrom]="item.route">
|
||||||
<ion-icon slot="end" [name]="item.icon"></ion-icon>
|
<ion-icon slot="end" [name]="$any(item.icon)"></ion-icon>
|
||||||
<ion-label> {{ item.translations[language]?.title | titlecase }} </ion-label>
|
<ion-label> {{ item.translations[language]?.title | titlecase }} </ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
</ion-menu-toggle>
|
</ion-menu-toggle>
|
||||||
@for (category of menu; track category; let isFirst = $first) {
|
@for (category of menu; track category; let isFirst = $first) {
|
||||||
<ion-tab-button [rootLink]="category.route" [redirectedFrom]="category.route" [tab]="category.title">
|
<ion-tab-button [rootLink]="category.route" [redirectedFrom]="category.route" [tab]="category.title">
|
||||||
<ion-icon [name]="category.icon"></ion-icon>
|
<ion-icon [name]="$any(category.icon)"></ion-icon>
|
||||||
<ion-label>{{ category.translations[language]?.title | titlecase }}</ion-label>
|
<ion-label>{{ category.translations[language]?.title | titlecase }}</ion-label>
|
||||||
</ion-tab-button>
|
</ion-tab-button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@if (link.icon) {
|
@if (link.icon) {
|
||||||
<ion-icon [name]="link.icon" [size]="36" color="dark"></ion-icon>
|
<ion-icon [name]="$any(link.icon)" [size]="36" color="dark"></ion-icon>
|
||||||
}
|
}
|
||||||
<ion-label>{{ 'name' | translateSimple: link }}</ion-label>
|
<ion-label>{{ 'name' | translateSimple: link }}</ion-label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
5
frontend/app/src/app/util/ion-icon/icon-match.d.mts
Normal file
5
frontend/app/src/app/util/ion-icon/icon-match.d.mts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export function matchTagProperties(tag: string): RegExp;
|
||||||
|
|
||||||
|
export function matchPropertyContent(properties: string[]): RegExp;
|
||||||
|
|
||||||
|
export function matchPropertyAccess(objectName: string): RegExp;
|
||||||
@@ -14,17 +14,24 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* @param {string} tag
|
||||||
*/
|
*/
|
||||||
export function matchTagProperties(tag: string) {
|
export function matchTagProperties(tag) {
|
||||||
return new RegExp(`(?<=<${tag})[\\s\\S]*?(?=>\\s*<\\/${tag}\\s*>)`, 'g');
|
return new RegExp(`(?<=<${tag})[\\s\\S]*?(?=>\\s*<\\/${tag}\\s*>)`, 'g');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* @param {string[]} properties
|
||||||
*/
|
*/
|
||||||
export function matchPropertyContent(properties: string[]) {
|
export function matchPropertyContent(properties) {
|
||||||
const names = properties.join('|');
|
const names = properties.join('|');
|
||||||
|
|
||||||
return new RegExp(`((?<=(${names})=")[\\w-]+(?="))|((?<=\\[(${names})]="')[\\w-]+(?='"))`, 'g');
|
return new RegExp(`((?<=(${names})=")[\\w-]+(?="))|((?<=\\[(${names})]="')[\\w-]+(?='"))`, 'g');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} objectName
|
||||||
|
*/
|
||||||
|
export function matchPropertyAccess(objectName) {
|
||||||
|
return new RegExp(`(?<=${objectName}\\s*\\.\\s*)\\w+`, 'g');
|
||||||
|
}
|
||||||
@@ -12,9 +12,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
/* eslint-disable unicorn/no-null */
|
import {matchPropertyAccess, matchPropertyContent, matchTagProperties} from './icon-match.mjs';
|
||||||
|
|
||||||
import {matchPropertyContent, matchTagProperties} from './icon-match';
|
|
||||||
|
|
||||||
describe('matchTagProperties', function () {
|
describe('matchTagProperties', function () {
|
||||||
const regex = matchTagProperties('test');
|
const regex = matchTagProperties('test');
|
||||||
@@ -59,3 +57,30 @@ describe('matchPropertyContent', function () {
|
|||||||
expect(`no="content" [no]="'content'"`.match(regex)).toEqual(null);
|
expect(`no="content" [no]="'content'"`.match(regex)).toEqual(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('matchPropertyAccess', function () {
|
||||||
|
const property = '0_20a_boninAo0_';
|
||||||
|
const object = 'test';
|
||||||
|
const regex = matchPropertyAccess(object);
|
||||||
|
|
||||||
|
it('should match property access', function () {
|
||||||
|
expect(`${object}.${property}`.match(regex)).toEqual([property]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect whitespace', function () {
|
||||||
|
expect(`${object}. ${property}`.match(regex)).toEqual([property]);
|
||||||
|
expect(`${object} .${property}`.match(regex)).toEqual([property]);
|
||||||
|
expect(`${object} . ${property}`.match(regex)).toEqual([property]);
|
||||||
|
expect(`${object} \n . \n ${property}`.match(regex)).toEqual([property]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include invalid trailing stuff', function () {
|
||||||
|
expect(`${object}.${property}!`.match(regex)).toEqual([property]);
|
||||||
|
expect(`${object}.${property}.`.match(regex)).toEqual([property]);
|
||||||
|
expect(`${object}.${property}-`.match(regex)).toEqual([property]);
|
||||||
|
expect(`${object}.${property}]`.match(regex)).toEqual([property]);
|
||||||
|
expect(`${object}.${property}}`.match(regex)).toEqual([property]);
|
||||||
|
expect(`${object}.${property};`.match(regex)).toEqual([property]);
|
||||||
|
expect(`${object}.${property}:`.match(regex)).toEqual([property]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -13,9 +13,8 @@
|
|||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
import {MaterialSymbol} from 'material-symbols';
|
||||||
* A noop function to aid parsing icon names
|
|
||||||
*/
|
export const SCIcon = new Proxy({} as {[key in MaterialSymbol]: key}, {
|
||||||
export function SCIcon(strings: TemplateStringsArray, ..._keys: string[]): string {
|
get: (_target, prop: string) => prop as MaterialSymbol,
|
||||||
return strings.join('');
|
});
|
||||||
}
|
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ export class IonBackButtonDirective extends IconReplacer implements OnInit {
|
|||||||
|
|
||||||
replace() {
|
replace() {
|
||||||
this.replaceIcon(this.host.querySelector('.button-inner'), {
|
this.replaceIcon(this.host.querySelector('.button-inner'), {
|
||||||
md: SCIcon`arrow_back`,
|
md: SCIcon.arrow_back,
|
||||||
ios: SCIcon`arrow_back_ios`,
|
ios: SCIcon.arrow_back_ios,
|
||||||
size: 24,
|
size: 24,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class IonBreadcrumbDirective extends IconReplacer {
|
|||||||
this.replaceIcon(
|
this.replaceIcon(
|
||||||
this.host.querySelector('span[part="separator"]'),
|
this.host.querySelector('span[part="separator"]'),
|
||||||
{
|
{
|
||||||
name: SCIcon`arrow_forward_ios`,
|
name: SCIcon.arrow_forward_ios,
|
||||||
size: 16,
|
size: 16,
|
||||||
style: `color: var(--ion-color-tint);`,
|
style: `color: var(--ion-color-tint);`,
|
||||||
},
|
},
|
||||||
@@ -37,7 +37,7 @@ export class IonBreadcrumbDirective extends IconReplacer {
|
|||||||
this.replaceIcon(
|
this.replaceIcon(
|
||||||
this.host.querySelector('button[part="collapsed-indicator"]'),
|
this.host.querySelector('button[part="collapsed-indicator"]'),
|
||||||
{
|
{
|
||||||
name: SCIcon`more_horiz`,
|
name: SCIcon.more_horiz,
|
||||||
size: 24,
|
size: 24,
|
||||||
},
|
},
|
||||||
'-collapsed',
|
'-collapsed',
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {IconComponent} from './icon.component';
|
import {IconComponent} from './icon.component';
|
||||||
import {IonIcon} from '@ionic/angular';
|
import {IonIcon} from '@ionic/angular';
|
||||||
|
import {MaterialSymbol} from 'material-symbols';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
@@ -40,7 +41,7 @@ const noopProperty = {
|
|||||||
selector: 'ion-icon',
|
selector: 'ion-icon',
|
||||||
})
|
})
|
||||||
export class IonIconDirective implements OnInit, OnDestroy, OnChanges {
|
export class IonIconDirective implements OnInit, OnDestroy, OnChanges {
|
||||||
@Input() name: string;
|
@Input() name: MaterialSymbol;
|
||||||
|
|
||||||
@Input() md: string;
|
@Input() md: string;
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class IonReorderDirective extends IconReplacer {
|
|||||||
|
|
||||||
replace() {
|
replace() {
|
||||||
this.replaceIcon(this.host, {
|
this.replaceIcon(this.host, {
|
||||||
name: SCIcon`reorder`,
|
name: SCIcon.reorder,
|
||||||
size: 24,
|
size: 24,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ export class IonSearchbarDirective extends IconReplacer {
|
|||||||
|
|
||||||
replace() {
|
replace() {
|
||||||
this.replaceIcon(this.host.querySelector('.searchbar-input-container'), {
|
this.replaceIcon(this.host.querySelector('.searchbar-input-container'), {
|
||||||
name: SCIcon`search`,
|
name: SCIcon.search,
|
||||||
size: 24,
|
size: 24,
|
||||||
});
|
});
|
||||||
this.replaceIcon(this.host.querySelector('.searchbar-clear-button'), {
|
this.replaceIcon(this.host.querySelector('.searchbar-clear-button'), {
|
||||||
name: SCIcon`close`,
|
name: SCIcon.close,
|
||||||
size: 24,
|
size: 24,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export const profilePageSections: SCSection[] = [
|
|||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
name: 'Favorites',
|
name: 'Favorites',
|
||||||
icon: SCIcon`grade`,
|
icon: SCIcon.grade,
|
||||||
link: ['/favorites'],
|
link: ['/favorites'],
|
||||||
translations: {
|
translations: {
|
||||||
de: {
|
de: {
|
||||||
@@ -73,7 +73,7 @@ export const profilePageSections: SCSection[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Schedule',
|
name: 'Schedule',
|
||||||
icon: SCIcon`calendar_today`,
|
icon: SCIcon.calendar_today,
|
||||||
link: ['/schedule'],
|
link: ['/schedule'],
|
||||||
translations: {
|
translations: {
|
||||||
de: {
|
de: {
|
||||||
@@ -84,7 +84,7 @@ export const profilePageSections: SCSection[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Course Catalog',
|
name: 'Course Catalog',
|
||||||
icon: SCIcon`inventory_2`,
|
icon: SCIcon.inventory_2,
|
||||||
link: ['/catalog'],
|
link: ['/catalog'],
|
||||||
translations: {
|
translations: {
|
||||||
de: {
|
de: {
|
||||||
@@ -95,7 +95,7 @@ export const profilePageSections: SCSection[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
icon: SCIcon`settings`,
|
icon: SCIcon.settings,
|
||||||
link: ['/settings'],
|
link: ['/settings'],
|
||||||
translations: {
|
translations: {
|
||||||
de: {
|
de: {
|
||||||
@@ -106,7 +106,7 @@ export const profilePageSections: SCSection[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Feedback',
|
name: 'Feedback',
|
||||||
icon: SCIcon`rate_review`,
|
icon: SCIcon.rate_review,
|
||||||
link: ['/feedback'],
|
link: ['/feedback'],
|
||||||
translations: {
|
translations: {
|
||||||
de: {
|
de: {
|
||||||
@@ -117,7 +117,7 @@ export const profilePageSections: SCSection[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'About',
|
name: 'About',
|
||||||
icon: SCIcon`info`,
|
icon: SCIcon.info,
|
||||||
link: ['/about'],
|
link: ['/about'],
|
||||||
translations: {
|
translations: {
|
||||||
de: {
|
de: {
|
||||||
@@ -140,7 +140,7 @@ export const profilePageSections: SCSection[] = [
|
|||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
name: 'Assessments',
|
name: 'Assessments',
|
||||||
icon: SCIcon`fact_check`,
|
icon: SCIcon.fact_check,
|
||||||
link: ['/assessments'],
|
link: ['/assessments'],
|
||||||
needsAuth: true,
|
needsAuth: true,
|
||||||
translations: {
|
translations: {
|
||||||
@@ -164,7 +164,7 @@ export const profilePageSections: SCSection[] = [
|
|||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
name: 'Library Catalog',
|
name: 'Library Catalog',
|
||||||
icon: SCIcon`local_library`,
|
icon: SCIcon.local_library,
|
||||||
link: ['/hebis-search'],
|
link: ['/hebis-search'],
|
||||||
translations: {
|
translations: {
|
||||||
de: {
|
de: {
|
||||||
@@ -175,7 +175,7 @@ export const profilePageSections: SCSection[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Library Account',
|
name: 'Library Account',
|
||||||
icon: SCIcon`badge`,
|
icon: SCIcon.badge,
|
||||||
needsAuth: true,
|
needsAuth: true,
|
||||||
link: ['/library-account/profile'],
|
link: ['/library-account/profile'],
|
||||||
translations: {
|
translations: {
|
||||||
@@ -187,7 +187,7 @@ export const profilePageSections: SCSection[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Orders & Reservations',
|
name: 'Orders & Reservations',
|
||||||
icon: SCIcon`collections_bookmark`,
|
icon: SCIcon.collections_bookmark,
|
||||||
needsAuth: true,
|
needsAuth: true,
|
||||||
link: ['/library-account/holds'],
|
link: ['/library-account/holds'],
|
||||||
translations: {
|
translations: {
|
||||||
@@ -199,7 +199,7 @@ export const profilePageSections: SCSection[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Checked out items',
|
name: 'Checked out items',
|
||||||
icon: SCIcon`library_books`,
|
icon: SCIcon.library_books,
|
||||||
needsAuth: true,
|
needsAuth: true,
|
||||||
link: ['/library-account/checked-out'],
|
link: ['/library-account/checked-out'],
|
||||||
translations: {
|
translations: {
|
||||||
@@ -211,7 +211,7 @@ export const profilePageSections: SCSection[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Fines',
|
name: 'Fines',
|
||||||
icon: SCIcon`request_page`,
|
icon: SCIcon.request_page,
|
||||||
needsAuth: true,
|
needsAuth: true,
|
||||||
link: ['/library-account/fines'],
|
link: ['/library-account/fines'],
|
||||||
translations: {
|
translations: {
|
||||||
|
|||||||
Reference in New Issue
Block a user