Compare commits

..

8 Commits

Author SHA1 Message Date
9c30211ba2 feat: splash transition
refactor: require app reload for setting changes

feat: require reload on setting changes

feat: new logo

feat: update to capacitor 5

feat: new logo

feat: update to capacitor 5

refactor: simplify settings provider
2024-01-03 12:27:42 +00:00
63a38e0077 feat: enable checkJs by default 2024-01-03 12:15:15 +00:00
c8b260201c feat: add direnv for nix
feat: update nix flake to not rely on buildFHSUserEnv
2024-01-03 12:15:15 +00:00
123c50d1af fix: backend tests break every year
refactor: update some backend unit tests
2024-01-03 12:57:24 +01:00
Rainer Killinger
d65e6351e9 fix: iOS build resources 2023-12-21 12:51:39 +01:00
Rainer Killinger
2c5d7403db refactor: add asdf tool versioning file 2023-12-21 12:25:38 +01:00
6ca03f463d fix: changeset crashes because it uses internal prettier version 2023-12-21 11:26:01 +01:00
Rainer Killinger
1f74a9bc82 refactor: overhaul minimal-deployment compose file 2023-12-19 16:52:58 +01:00
291 changed files with 8953 additions and 10630 deletions

View File

@@ -0,0 +1,5 @@
---
"@openstapps/prettier-config": patch
---
Update Prettier to 3.1.1

View File

@@ -0,0 +1,5 @@
---
"@openstapps/backend": patch
---
Backend unit tests break every year

View File

@@ -0,0 +1,9 @@
---
'@openstapps/app': minor
---
Require full reload for setting & language changes
Setting changes are relatively rare, so it makes little sense
going through the effort of ensuring everything is reactive to
language changes as well as creating all the pipe bindings etc.

View File

@@ -2,7 +2,7 @@
/** @type {import('syncpack').RcFile} */ /** @type {import('syncpack').RcFile} */
const config = { const config = {
semverGroups: [{range: ''}], semverRange: '',
source: ['package.json', '**/package.json'], source: ['package.json', '**/package.json'],
indent: ' ', indent: ' ',
sortFirst: [ sortFirst: [
@@ -49,7 +49,7 @@ const config = {
{ {
label: 'Packages should use workspace version', label: 'Packages should use workspace version',
dependencies: ['@openstapps/**'], dependencies: ['@openstapps/**'],
dependencyTypes: ['prod', 'dev', 'peer'], dependencyTypes: ['prod', 'dev'],
packages: ['**'], packages: ['**'],
pinVersion: 'workspace:*', pinVersion: 'workspace:*',
}, },

View File

@@ -1,3 +1,3 @@
nodejs 18.19.1 nodejs 18.16.1
pnpm 8.15.4 pnpm 8.8.0
python 3.11.5 python 3.11.5

View File

@@ -1,4 +1,4 @@
# Open StApps Monorepo # <img src="logo-bg.svg" height="24"> Open StApps Monorepo
Refer to the [contribution guide](./CONTRIBUTING.md) Refer to the [contribution guide](./CONTRIBUTING.md)

View File

@@ -1,20 +1,5 @@
# @openstapps/backend # @openstapps/backend
## 3.2.0
### Minor Changes
- 912ae422: Add the ability to filter by existence of a field
### Patch Changes
- 689ac68b: pin alpine version to 3.18 and add healthchecks
- e8d72683: Backend unit tests break every year
- Updated dependencies [912ae422]
- @openstapps/core@4.0.0
- @openstapps/core-tools@3.0.0
- @openstapps/logger@3.0.0
## 3.1.2 ## 3.1.2
### Patch Changes ### Patch Changes

View File

@@ -9,6 +9,4 @@ ENV NODE_ENV=production
WORKDIR /app WORKDIR /app
EXPOSE 3000 EXPOSE 3000
HEALTHCHECK --interval=10s --timeout=10s --start-period=10s --retries=12 CMD curl -s --fail --request POST --data '{}' --header 'Content-Type: application/json' http://localhost:3000/ >/dev/null || exit 1
ENTRYPOINT ["node", "app.js"] ENTRYPOINT ["node", "app.js"]

View File

@@ -1,7 +1,7 @@
{ {
"name": "@openstapps/backend", "name": "@openstapps/backend",
"description": "A reference implementation for a StApps backend", "description": "A reference implementation for a StApps backend",
"version": "3.2.0", "version": "3.1.2",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
@@ -64,7 +64,7 @@
"express-prom-bundle": "6.6.0", "express-prom-bundle": "6.6.0",
"express-promise-router": "4.1.1", "express-promise-router": "4.1.1",
"got": "12.6.0", "got": "12.6.0",
"moment": "2.30.1", "moment": "2.29.4",
"morgan": "1.10.0", "morgan": "1.10.0",
"nock": "13.3.1", "nock": "13.3.1",
"node-cache": "5.1.2", "node-cache": "5.1.2",
@@ -98,9 +98,9 @@
"sinon": "15.0.4", "sinon": "15.0.4",
"sinon-express-mock": "2.2.1", "sinon-express-mock": "2.2.1",
"supertest": "6.3.3", "supertest": "6.3.3",
"ts-node": "10.9.2", "ts-node": "10.9.1",
"tsup": "6.7.0", "tsup": "6.7.0",
"typescript": "5.4.2" "typescript": "5.1.6"
}, },
"tsup": { "tsup": {
"entry": [ "entry": [

View File

@@ -21,31 +21,16 @@ import {QueryDslSpecificQueryContainer} from '../../types/util.js';
*/ */
export function buildValueFilter( export function buildValueFilter(
filter: SCSearchValueFilter, filter: SCSearchValueFilter,
): ): QueryDslSpecificQueryContainer<'term'> | QueryDslSpecificQueryContainer<'terms'> {
| QueryDslSpecificQueryContainer<'exists'> return Array.isArray(filter.arguments.value)
| QueryDslSpecificQueryContainer<'term'> ? {
| QueryDslSpecificQueryContainer<'terms'> { terms: {
switch (typeof filter.arguments.value) { [`${filter.arguments.field}.raw`]: filter.arguments.value,
case 'undefined': {
return {
exists: {
field: filter.arguments.field,
}, },
}; }
} : {
case 'string': {
return {
term: { term: {
[`${filter.arguments.field}.raw`]: filter.arguments.value, [`${filter.arguments.field}.raw`]: filter.arguments.value,
}, },
}; };
}
case 'object': {
return {
terms: {
[`${filter.arguments.field}.raw`]: filter.arguments.value,
},
};
}
}
} }

View File

@@ -2,7 +2,9 @@
"extends": "@openstapps/tsconfig", "extends": "@openstapps/tsconfig",
"compilerOptions": { "compilerOptions": {
"resolveJsonModule": true, "resolveJsonModule": true,
"useUnknownInCatchVariables": false "useUnknownInCatchVariables": false,
"allowJs": true,
"checkJs": true
}, },
"exclude": ["lib", "app.js"] "exclude": ["app.js", "lib/"]
} }

View File

@@ -1,11 +1,5 @@
# @openstapps/database # @openstapps/database
## 3.2.0
### Patch Changes
- 689ac68b: pin alpine version to 3.18 and add healthchecks
## 3.0.0 ## 3.0.0
### Patch Changes ### Patch Changes

View File

@@ -14,6 +14,4 @@ RUN chown elasticsearch:elasticsearch config/elasticsearch.yml
USER elasticsearch USER elasticsearch
HEALTHCHECK --interval=10s --timeout=10s --start-period=60s --retries=12 CMD curl --fail -s http://localhost:9200/ >/dev/null || exit 1
CMD ["/usr/share/elasticsearch/bin/elasticsearch"] CMD ["/usr/share/elasticsearch/bin/elasticsearch"]

View File

@@ -3,4 +3,3 @@ discovery.type: "single-node"
cluster.routing.allocation.disk.threshold_enabled: false cluster.routing.allocation.disk.threshold_enabled: false
network.bind_host: 0.0.0.0 network.bind_host: 0.0.0.0
xpack.security.enabled: false xpack.security.enabled: false
ingest.geoip.downloader.enabled: false

View File

@@ -1,6 +1,6 @@
{ {
"name": "@openstapps/database", "name": "@openstapps/database",
"version": "3.2.0", "version": "3.0.0",
"private": true, "private": true,
"files": [ "files": [
"config", "config",

View File

@@ -16,7 +16,7 @@
// ESM is not supported, and cts is not detected, so we use type-checked cjs instead. // ESM is not supported, and cts is not detected, so we use type-checked cjs instead.
/** @type {import('../src/common').ConfigFile} */ /** @type {import('../src/common').ConfigFile} */
module.exports = { const configFile = {
activeVersions: ['1\\.0\\.\\d+', '2\\.0\\.\\d+'], activeVersions: ['1\\.0\\.\\d+', '2\\.0\\.\\d+'],
hiddenRoutes: ['/bulk'], hiddenRoutes: ['/bulk'],
logFormat: 'default', logFormat: 'default',
@@ -31,3 +31,5 @@ module.exports = {
dhparam: '/etc/nginx/certs/dhparam.pem', dhparam: '/etc/nginx/certs/dhparam.pem',
}, },
}; };
export default configFile;

View File

@@ -50,8 +50,8 @@
"dockerode": "3.3.5", "dockerode": "3.3.5",
"is-cidr": "4.0.2", "is-cidr": "4.0.2",
"mustache": "4.2.0", "mustache": "4.2.0",
"semver": "7.6.0", "semver": "7.5.4",
"typescript": "5.4.2" "typescript": "5.1.6"
}, },
"devDependencies": { "devDependencies": {
"@openstapps/api-cli": "workspace:*", "@openstapps/api-cli": "workspace:*",
@@ -65,7 +65,7 @@
"@types/mustache": "4.2.2", "@types/mustache": "4.2.2",
"@types/node": "18.15.3", "@types/node": "18.15.3",
"@types/proxyquire": "1.3.28", "@types/proxyquire": "1.3.28",
"@types/semver": "7.5.8", "@types/semver": "7.5.6",
"@types/sha1": "1.1.3", "@types/sha1": "1.1.3",
"@types/sinon": "10.0.14", "@types/sinon": "10.0.14",
"@types/sinon-chai": "3.2.9", "@types/sinon-chai": "3.2.9",
@@ -75,7 +75,7 @@
"mocha-junit-reporter": "2.2.0", "mocha-junit-reporter": "2.2.0",
"sinon": "15.0.4", "sinon": "15.0.4",
"sinon-chai": "3.7.0", "sinon-chai": "3.7.0",
"ts-node": "10.9.2", "ts-node": "10.9.1",
"tsup": "6.7.0" "tsup": "6.7.0"
}, },
"tsup": { "tsup": {

View File

@@ -1,4 +1,4 @@
{ {
"extends": "@openstapps/tsconfig", "extends": "@openstapps/tsconfig",
"exclude": ["config", "lib", "app.js"] "exclude": ["./config/", "./lib/"]
} }

View File

@@ -18,15 +18,16 @@
"devDependencies": { "devDependencies": {
"@openstapps/tsconfig": "workspace:*", "@openstapps/tsconfig": "workspace:*",
"@types/node": "18.15.3", "@types/node": "18.15.3",
"eslint": "8.57.0", "eslint": "8.43.0",
"typescript": "5.4.2" "typescript": "5.1.6"
}, },
"peerDependencies": { "peerDependencies": {
"@typescript-eslint/eslint-plugin": "7.2.0", "@typescript-eslint/eslint-plugin": "5.60.1",
"@typescript-eslint/parser": "7.2.0", "@typescript-eslint/parser": "5.60.1",
"eslint": "8.57.0", "eslint": "8.43.0",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "8.8.0",
"eslint-plugin-jsdoc": "48.2.1", "eslint-plugin-jsdoc": "46.4.2",
"eslint-plugin-unicorn": "51.0.1" "eslint-plugin-prettier": "4.2.1",
"eslint-plugin-unicorn": "47.0.0"
} }
} }

View File

@@ -1,11 +1,5 @@
# @openstapps/prettier-config # @openstapps/prettier-config
## 3.2.0
### Patch Changes
- dbb55850: Update Prettier to 3.1.1
## 3.0.0 ## 3.0.0
### Major Changes ### Major Changes
@@ -36,7 +30,7 @@
```js ```js
#!/usr/bin/env node #!/usr/bin/env node
import "./lib/app.js"; import './lib/app.js';
``` ```
- 64caebaf: Migrate to ESM - 64caebaf: Migrate to ESM
@@ -75,14 +69,11 @@
- 64caebaf: Migrated changelogs to changeset format - 64caebaf: Migrated changelogs to changeset format
```js ```js
import fs from "fs"; import fs from 'fs';
const path = "packages/logger/CHANGELOG.md"; const path = 'packages/logger/CHANGELOG.md';
fs.writeFileSync( fs.writeFileSync(path, fs.readFileSync(path, 'utf8').replace(/^#+\s+\[/gm, '## ['));
path,
fs.readFileSync(path, "utf8").replace(/^#+\s+\[/gm, "## ["),
);
``` ```
- 98546a97: Migrate away from @openstapps/configuration - 98546a97: Migrate away from @openstapps/configuration
@@ -124,7 +115,7 @@
```js ```js
#!/usr/bin/env node #!/usr/bin/env node
import "./lib/app.js"; import './lib/app.js';
``` ```
- 64caebaf: Migrate to ESM - 64caebaf: Migrate to ESM
@@ -163,14 +154,11 @@
- 64caebaf: Migrated changelogs to changeset format - 64caebaf: Migrated changelogs to changeset format
```js ```js
import fs from "fs"; import fs from 'fs';
const path = "packages/logger/CHANGELOG.md"; const path = 'packages/logger/CHANGELOG.md';
fs.writeFileSync( fs.writeFileSync(path, fs.readFileSync(path, 'utf8').replace(/^#+\s+\[/gm, '## ['));
path,
fs.readFileSync(path, "utf8").replace(/^#+\s+\[/gm, "## ["),
);
``` ```
- 98546a97: Migrate away from @openstapps/configuration - 98546a97: Migrate away from @openstapps/configuration

View File

@@ -1,7 +1,7 @@
{ {
"name": "@openstapps/prettier-config", "name": "@openstapps/prettier-config",
"description": "StApps Prettier Config", "description": "StApps Prettier Config",
"version": "3.2.0", "version": "3.0.0",
"type": "module", "type": "module",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"repository": "git@gitlab.com:openstapps/prettier-config.git", "repository": "git@gitlab.com:openstapps/prettier-config.git",

View File

@@ -43,8 +43,8 @@
"@openstapps/logger": "workspace:*", "@openstapps/logger": "workspace:*",
"@slack/web-api": "6.8.1", "@slack/web-api": "6.8.1",
"commander": "10.0.0", "commander": "10.0.0",
"date-fns": "3.6.0", "date-fns": "2.30.0",
"glob": "10.3.10", "glob": "10.2.7",
"mustache": "4.2.0" "mustache": "4.2.0"
}, },
"devDependencies": { "devDependencies": {
@@ -53,7 +53,7 @@
"@openstapps/tsconfig": "workspace:*", "@openstapps/tsconfig": "workspace:*",
"@types/chai": "4.3.5", "@types/chai": "4.3.5",
"@types/chai-as-promised": "7.1.5", "@types/chai-as-promised": "7.1.5",
"@types/glob": "8.1.0", "@types/glob": "8.0.1",
"@types/mocha": "10.0.1", "@types/mocha": "10.0.1",
"@types/mustache": "4.2.2", "@types/mustache": "4.2.2",
"@types/node": "18.15.3", "@types/node": "18.15.3",
@@ -63,9 +63,9 @@
"chai-as-promised": "7.1.1", "chai-as-promised": "7.1.1",
"mocha": "10.2.0", "mocha": "10.2.0",
"mocha-junit-reporter": "2.2.0", "mocha-junit-reporter": "2.2.0",
"ts-node": "10.9.2", "ts-node": "10.9.1",
"tsup": "6.7.0", "tsup": "6.7.0",
"typescript": "5.4.2" "typescript": "5.1.6"
}, },
"tsup": { "tsup": {
"entry": [ "entry": [

View File

@@ -1,4 +1,3 @@
{ {
"extends": "@openstapps/tsconfig", "extends": "@openstapps/tsconfig"
"exclude": ["lib", "app.js"]
} }

View File

@@ -1,14 +1,5 @@
# @openstapps/minimal-connector # @openstapps/minimal-connector
## 3.2.0
### Patch Changes
- Updated dependencies [912ae422]
- @openstapps/core@4.0.0
- @openstapps/api@4.0.0
- @openstapps/logger@3.0.0
## 3.1.1 ## 3.1.1
### Patch Changes ### Patch Changes

View File

@@ -1,7 +1,7 @@
{ {
"name": "@openstapps/minimal-connector", "name": "@openstapps/minimal-connector",
"description": "This is a minimal connector which serves as an example", "description": "This is a minimal connector which serves as an example",
"version": "3.2.0", "version": "3.1.1",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
@@ -53,9 +53,9 @@
"mocha": "10.2.0", "mocha": "10.2.0",
"mocha-junit-reporter": "2.2.0", "mocha-junit-reporter": "2.2.0",
"nock": "13.3.1", "nock": "13.3.1",
"ts-node": "10.9.2", "ts-node": "10.9.1",
"tsup": "6.7.0", "tsup": "6.7.0",
"typescript": "5.4.2" "typescript": "5.1.6"
}, },
"tsup": { "tsup": {
"entry": [ "entry": [

View File

@@ -1,4 +1,3 @@
{ {
"extends": "@openstapps/tsconfig", "extends": "@openstapps/tsconfig"
"exclude": ["lib", "app.js"]
} }

View File

@@ -1,16 +1,5 @@
# @openstapps/minimal-plugin # @openstapps/minimal-plugin
## 3.2.0
### Patch Changes
- Updated dependencies [912ae422]
- @openstapps/core@4.0.0
- @openstapps/api@4.0.0
- @openstapps/api-plugin@4.0.0
- @openstapps/core-tools@3.0.0
- @openstapps/logger@3.0.0
## 3.1.1 ## 3.1.1
### Patch Changes ### Patch Changes

View File

@@ -1,7 +1,7 @@
{ {
"name": "@openstapps/minimal-plugin", "name": "@openstapps/minimal-plugin",
"description": "Minimal Plugin", "description": "Minimal Plugin",
"version": "3.2.0", "version": "3.1.1",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
@@ -35,7 +35,7 @@
"@openstapps/logger": "workspace:*", "@openstapps/logger": "workspace:*",
"commander": "10.0.0", "commander": "10.0.0",
"express": "4.18.2", "express": "4.18.2",
"ts-node": "10.9.2" "ts-node": "10.9.1"
}, },
"devDependencies": { "devDependencies": {
"@openstapps/eslint-config": "workspace:*", "@openstapps/eslint-config": "workspace:*",
@@ -44,7 +44,7 @@
"@types/express": "4.17.17", "@types/express": "4.17.17",
"@types/node": "18.15.3", "@types/node": "18.15.3",
"tsup": "6.7.0", "tsup": "6.7.0",
"typescript": "5.4.2" "typescript": "5.1.6"
}, },
"tsup": { "tsup": {
"entry": [ "entry": [

View File

@@ -1,4 +1,3 @@
{ {
"extends": "@openstapps/tsconfig", "extends": "@openstapps/tsconfig"
"exclude": ["lib", "app.js"]
} }

View File

@@ -5,4 +5,10 @@
# You can see what browsers were selected by your queries by running: # You can see what browsers were selected by your queries by running:
# npx browserslist # npx browserslist
> 0.5% in DE and last 2 major versions and supports es6 and not dead > 0.5%
last 2 versions
Firefox ESR
not dead
not kaios 2.5
not op_mini all
not IE 9-11

View File

@@ -1,15 +1,5 @@
# @openstapps/app # @openstapps/app
## 3.2.0
### Patch Changes
- 689ac68b: pin alpine version to 3.18 and add healthchecks
- Updated dependencies [912ae422]
- @openstapps/core@4.0.0
- @openstapps/api@4.0.0
- @openstapps/collection-utils@3.0.0
## 3.1.2 ## 3.1.2
### Patch Changes ### Patch Changes

View File

@@ -1,7 +1,7 @@
# Creates a docker image with only the app as an executable unit # Creates a docker image with only the app as an executable unit
# Dependencies need to be installed beforehand # Dependencies need to be installed beforehand
# Needs to be build beforehand # Needs to be build beforehand
FROM node:18-alpine3.18 FROM node:18-alpine
WORKDIR /app WORKDIR /app
COPY www/ /app/www COPY www/ /app/www

View File

@@ -97,20 +97,20 @@
"serve": { "serve": {
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"options": { "options": {
"buildTarget": "app:build" "browserTarget": "app:build"
}, },
"configurations": { "configurations": {
"production": { "production": {
"buildTarget": "app:build:production" "browserTarget": "app:build:production"
}, },
"development": { "development": {
"buildTarget": "app:build:development" "browserTarget": "app:build:development"
}, },
"ci": { "ci": {
"buildTarget": "app:build" "browserTarget": "app:build"
}, },
"fake": { "fake": {
"buildTarget": "app:build:fake" "browserTarget": "app:build:fake"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"

View File

@@ -173,7 +173,7 @@ describe('dashboard', async function () {
cy.visit('/overview'); cy.visit('/overview');
cy.get('ion-searchbar').click({scrollBehavior: 'center'}); cy.get('ion-searchbar').click({scrollBehavior: 'center'});
cy.url().should('include', '/search'); cy.url().should('eq', Cypress.config().baseUrl + '/search');
cy.get('ion-searchbar').should('not.have.value'); cy.get('ion-searchbar').should('not.have.value');
cy.get('ion-searchbar input.searchbar-input').should('have.focus'); cy.get('ion-searchbar input.searchbar-input').should('have.focus');

View File

@@ -1,7 +1,7 @@
{ {
"name": "@openstapps/app", "name": "@openstapps/app",
"description": "The generic app tailored to fulfill needs of German universities, written using Ionic Framework.", "description": "The generic app tailored to fulfill needs of German universities, written using Ionic Framework.",
"version": "3.2.0", "version": "3.1.2",
"private": true, "private": true,
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"author": "Karl-Philipp Wulfert <krlwlfrt@gmail.com>", "author": "Karl-Philipp Wulfert <krlwlfrt@gmail.com>",
@@ -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": "ts-node-esm scripts/check-icon-correctness.ts",
"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",
@@ -52,34 +52,34 @@
"test:integration": "sh integration-test.sh" "test:integration": "sh integration-test.sh"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "17.3.0", "@angular/animations": "16.1.4",
"@angular/cdk": "17.3.0", "@angular/cdk": "16.1.4",
"@angular/common": "17.3.0", "@angular/common": "16.1.4",
"@angular/core": "17.3.0", "@angular/core": "16.1.4",
"@angular/forms": "17.3.0", "@angular/forms": "16.1.4",
"@angular/platform-browser": "17.3.0", "@angular/platform-browser": "16.1.4",
"@angular/router": "17.3.0", "@angular/router": "16.1.4",
"@asymmetrik/ngx-leaflet": "17.0.0", "@asymmetrik/ngx-leaflet": "16.0.1",
"@asymmetrik/ngx-leaflet-markercluster": "17.0.0", "@asymmetrik/ngx-leaflet-markercluster": "16.0.0",
"@awesome-cordova-plugins/calendar": "6.6.0", "@awesome-cordova-plugins/calendar": "5.45.0",
"@awesome-cordova-plugins/core": "6.6.0", "@awesome-cordova-plugins/core": "5.45.0",
"@capacitor/app": "5.0.7", "@capacitor/app": "5.0.6",
"@capacitor/browser": "5.2.0", "@capacitor/browser": "5.1.0",
"@capacitor/clipboard": "5.0.7", "@capacitor/clipboard": "5.0.6",
"@capacitor/core": "5.7.3", "@capacitor/core": "5.5.0",
"@capacitor/device": "5.0.7", "@capacitor/device": "5.0.6",
"@capacitor/dialog": "5.0.7", "@capacitor/dialog": "5.0.6",
"@capacitor/filesystem": "5.2.1", "@capacitor/filesystem": "5.1.4",
"@capacitor/geolocation": "5.0.7", "@capacitor/geolocation": "5.0.6",
"@capacitor/haptics": "5.0.7", "@capacitor/haptics": "5.0.6",
"@capacitor/keyboard": "5.0.8", "@capacitor/keyboard": "5.0.6",
"@capacitor/local-notifications": "5.0.7", "@capacitor/local-notifications": "5.0.6",
"@capacitor/network": "5.0.7", "@capacitor/network": "5.0.6",
"@capacitor/preferences": "5.0.7", "@capacitor/preferences": "5.0.6",
"@capacitor/share": "5.0.7", "@capacitor/share": "5.0.6",
"@capacitor/splash-screen": "5.0.7", "@capacitor/splash-screen": "5.0.6",
"@ionic-native/core": "5.36.0", "@ionic-native/core": "5.36.0",
"@ionic/angular": "7.8.0", "@ionic/angular": "7.1.3",
"@ionic/storage-angular": "4.0.0", "@ionic/storage-angular": "4.0.0",
"@ngx-translate/core": "15.0.0", "@ngx-translate/core": "15.0.0",
"@ngx-translate/http-loader": "8.0.0", "@ngx-translate/http-loader": "8.0.0",
@@ -87,104 +87,105 @@
"@openstapps/api": "workspace:*", "@openstapps/api": "workspace:*",
"@openstapps/collection-utils": "workspace:*", "@openstapps/collection-utils": "workspace:*",
"@openstapps/core": "workspace:*", "@openstapps/core": "workspace:*",
"@transistorsoft/capacitor-background-fetch": "5.2.0", "@transistorsoft/capacitor-background-fetch": "5.1.1",
"@types/dom-view-transitions": "1.0.4", "@types/dom-view-transitions": "1.0.1",
"capacitor-secure-storage-plugin": "0.9.0", "capacitor-secure-storage-plugin": "0.9.0",
"cordova-plugin-calendar": "5.1.6", "cordova-plugin-calendar": "5.1.6",
"date-fns": "3.6.0", "date-fns": "2.30.0",
"deepmerge": "4.3.1", "deepmerge": "4.3.1",
"fast-deep-equal": "3.1.3",
"form-data": "4.0.0", "form-data": "4.0.0",
"geojson": "0.5.0", "geojson": "0.5.0",
"ionic-appauth": "0.9.0", "ionic-appauth": "0.9.0",
"jsonpath-plus": "6.0.1", "jsonpath-plus": "6.0.1",
"leaflet": "1.9.4", "leaflet": "1.9.3",
"leaflet.markercluster": "1.5.3", "leaflet.markercluster": "1.5.3",
"material-symbols": "0.17.0", "material-symbols": "0.10.0",
"moment": "2.30.1", "moment": "2.29.4",
"ngx-date-fns": "11.0.0", "ngx-date-fns": "10.0.1",
"ngx-logger": "5.0.12", "ngx-logger": "5.0.12",
"ngx-markdown": "17.1.1", "ngx-markdown": "16.0.0",
"ngx-moment": "6.0.2", "ngx-moment": "6.0.2",
"opening_hours": "3.8.0", "opening_hours": "3.8.0",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"semver": "7.6.0", "semver": "7.5.4",
"swiper": "8.4.5", "swiper": "8.4.5",
"tslib": "2.6.2", "tslib": "2.4.1",
"zone.js": "0.14.4" "zone.js": "0.13.1"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/architect": "0.1703.0", "@angular-devkit/architect": "0.1601.4",
"@angular-devkit/build-angular": "17.3.0", "@angular-devkit/build-angular": "16.1.4",
"@angular-devkit/core": "17.3.0", "@angular-devkit/core": "16.1.4",
"@angular-devkit/schematics": "17.3.0", "@angular-devkit/schematics": "16.1.4",
"@angular-eslint/builder": "17.3.0", "@angular-eslint/builder": "16.1.0",
"@angular-eslint/eslint-plugin": "17.3.0", "@angular-eslint/eslint-plugin": "16.1.0",
"@angular-eslint/eslint-plugin-template": "17.3.0", "@angular-eslint/eslint-plugin-template": "16.1.0",
"@angular-eslint/schematics": "17.3.0", "@angular-eslint/schematics": "16.1.0",
"@angular-eslint/template-parser": "17.3.0", "@angular-eslint/template-parser": "16.1.0",
"@angular/cli": "17.3.0", "@angular/cli": "16.1.4",
"@angular/compiler": "17.3.0", "@angular/compiler": "16.1.4",
"@angular/compiler-cli": "17.3.0", "@angular/compiler-cli": "16.1.4",
"@angular/language-server": "17.3.0", "@angular/language-service": "16.1.4",
"@angular/language-service": "17.3.0", "@angular/platform-browser-dynamic": "16.1.4",
"@angular/platform-browser-dynamic": "17.3.0", "@capacitor/android": "5.5.0",
"@capacitor/android": "5.7.3", "@capacitor/assets": "3.0.1",
"@capacitor/assets": "3.0.4", "@capacitor/cli": "5.5.0",
"@capacitor/cli": "5.7.3", "@capacitor/ios": "5.5.0",
"@capacitor/ios": "5.7.3", "@compodoc/compodoc": "1.1.19",
"@compodoc/compodoc": "1.1.23", "@cypress/schematic": "1.7.0",
"@cypress/schematic": "2.5.1", "@ionic/angular-toolkit": "10.0.0",
"@ionic/angular-toolkit": "11.0.1", "@ionic/cli": "7.1.1",
"@ionic/cli": "7.2.0",
"@openstapps/prettier-config": "workspace:*", "@openstapps/prettier-config": "workspace:*",
"@openstapps/tsconfig": "workspace:*", "@openstapps/tsconfig": "workspace:*",
"@types/fontkit": "2.0.7", "@types/fontkit": "1.8.0",
"@types/geojson": "1.0.6", "@types/geojson": "1.0.6",
"@types/glob": "8.1.0", "@types/glob": "8.0.1",
"@types/jasmine": "5.1.4", "@types/jasmine": "4.3.1",
"@types/jasminewd2": "2.0.13", "@types/jasminewd2": "2.0.10",
"@types/jsonpath": "0.2.0", "@types/jsonpath": "0.2.0",
"@types/karma": "6.3.8", "@types/karma": "6.3.4",
"@types/karma-coverage": "2.0.3", "@types/karma-coverage": "2.0.1",
"@types/karma-jasmine": "4.0.5", "@types/karma-jasmine": "4.0.2",
"@types/leaflet": "1.9.8", "@types/leaflet": "1.9.0",
"@types/leaflet.markercluster": "1.5.4", "@types/leaflet.markercluster": "1.5.1",
"@types/node": "18.15.3", "@types/node": "18.15.3",
"@types/semver": "7.5.8", "@types/semver": "7.5.6",
"@typescript-eslint/eslint-plugin": "7.2.0", "@typescript-eslint/eslint-plugin": "5.60.1",
"@typescript-eslint/parser": "7.2.0", "@typescript-eslint/parser": "5.60.1",
"cordova-res": "0.15.4", "cordova-res": "0.15.4",
"cypress": "13.7.0", "cypress": "13.2.0",
"eslint": "8.57.0", "eslint": "8.43.0",
"eslint-plugin-jsdoc": "48.2.1", "eslint-plugin-jsdoc": "46.4.2",
"eslint-plugin-prettier": "5.1.3", "eslint-plugin-prettier": "4.2.1",
"eslint-plugin-unicorn": "51.0.1", "eslint-plugin-unicorn": "47.0.0",
"fast-deep-equal": "3.1.3", "fast-deep-equal": "3.1.3",
"fontkit": "2.0.2", "fontkit": "2.0.2",
"glob": "10.3.10", "glob": "10.2.7",
"http-server": "14.1.1", "http-server": "14.1.1",
"is-docker": "2.2.1", "is-docker": "2.2.1",
"jasmine-core": "5.1.2", "jasmine-core": "5.0.1",
"jasmine-spec-reporter": "7.0.0", "jasmine-spec-reporter": "7.0.0",
"jetifier": "2.0.0", "jetifier": "2.0.0",
"junit-report-merger": "6.0.3", "junit-report-merger": "6.0.2",
"karma": "6.4.3", "karma": "6.4.2",
"karma-chrome-launcher": "3.2.0", "karma-chrome-launcher": "3.2.0",
"karma-coverage": "2.2.1", "karma-coverage": "2.2.1",
"karma-jasmine": "5.1.0", "karma-jasmine": "5.1.0",
"karma-junit-reporter": "2.0.1", "karma-junit-reporter": "2.0.1",
"karma-mocha-reporter": "2.2.5", "karma-mocha-reporter": "2.2.5",
"license-checker": "25.0.1", "license-checker": "25.0.1",
"stylelint": "16.2.1", "stylelint": "15.10.1",
"stylelint-config-clean-order": "5.4.1", "stylelint-config-clean-order": "5.0.1",
"stylelint-config-prettier-scss": "1.0.0", "stylelint-config-prettier-scss": "1.0.0",
"stylelint-config-recommended-scss": "14.0.0", "stylelint-config-recommended-scss": "12.0.0",
"stylelint-config-standard-scss": "13.0.0", "stylelint-config-standard-scss": "10.0.0",
"surge": "0.23.1", "surge": "0.23.1",
"ts-node": "10.9.2", "ts-node": "10.9.1",
"typescript": "5.4.2", "typescript": "5.1.6",
"webpack-bundle-analyzer": "4.10.1" "webpack-bundle-analyzer": "4.7.0"
}, },
"prettier": "@openstapps/prettier-config",
"cordova": { "cordova": {
"plugins": {}, "plugins": {},
"platforms": [ "platforms": [

View File

@@ -1,4 +1,3 @@
// @ts-check
/* /*
* Copyright (C) 2022 StApps * Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it * This program is free software: you can redistribute it and/or modify it
@@ -26,6 +25,7 @@ const config = {
}, },
}, },
], ],
ignorePath: ['.prettierignore', '../../.gitignore'],
}; };
export default config; export default config;

View File

@@ -21,7 +21,7 @@ import {
SCThingOriginType, SCThingOriginType,
SCThingType, SCThingType,
} from '@openstapps/core'; } from '@openstapps/core';
import {CORE_VERSION} from '@openstapps/core'; import packageInfo from '@openstapps/core/package.json';
import {Polygon} from 'geojson'; import {Polygon} from 'geojson';
// provides sample aggregations to be used in tests or backendless development // provides sample aggregations to be used in tests or backendless development
@@ -195,7 +195,7 @@ export const sampleIndexResponse: SCIndexResponse = {
}, },
auth: {}, auth: {},
backend: { backend: {
SCVersion: CORE_VERSION, SCVersion: packageInfo.version,
externalRequestTimeout: 5000, externalRequestTimeout: 5000,
hiddenTypes: [SCThingType.DateSeries, SCThingType.Diff, SCThingType.Floor], hiddenTypes: [SCThingType.DateSeries, SCThingType.Diff, SCThingType.Floor],
mappingIgnoredTags: [], mappingIgnoredTags: [],

View File

@@ -14,7 +14,7 @@
*/ */
import {AnimationBuilder, AnimationController} from '@ionic/angular'; import {AnimationBuilder, AnimationController} from '@ionic/angular';
import {AnimationOptions} from '@ionic/angular/common/providers/nav-controller'; import {AnimationOptions} from '@ionic/angular/providers/nav-controller';
import {iosDuration, iosEasing, mdDuration, mdEasing} from './easings'; import {iosDuration, iosEasing, mdDuration, mdEasing} from './easings';
/** /**

View File

@@ -0,0 +1,80 @@
import {Animation, AnimationController} from '@ionic/angular';
import {iosDuration, iosEasing, mdDuration, mdEasing} from './easings';
/**
* Splash screen animation
*/
export function splashAnimation(animationCtl: AnimationController): Animation {
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
return animationCtl
.create()
.fromTo('opacity', 0, 1)
.duration(150)
.beforeClearStyles(['visibility'])
.addElement(document.querySelector('ion-app')!);
}
const isMd = document.querySelector('ion-app.md') !== null;
const navElement = document.querySelector('stapps-navigation-tabs')!;
const navBounds = navElement.getBoundingClientRect();
let horizontal = navBounds.width < navBounds.height;
if (window.getComputedStyle(navElement).display === 'none') {
horizontal = true;
}
const translate = (amount: number, unit = 'px') =>
`translate${horizontal ? 'X' : 'Y'}(${horizontal ? amount * -1 : amount}${unit})`;
const duration = 2 * (isMd ? mdDuration : iosDuration);
const animation = animationCtl
.create()
.duration(duration)
.easing(isMd ? mdEasing : iosEasing)
.addAnimation(
animationCtl.create().beforeClearStyles(['visibility']).addElement(document.querySelector('ion-app')!),
)
.addAnimation(
animationCtl
.create()
.fromTo('transform', translate(horizontal ? 64 : 192), translate(0))
.fromTo('opacity', 0, 1)
.addElement(document.querySelector('stapps-navigation > ion-split-pane')!),
)
.addAnimation(
animationCtl
.create()
.fromTo('transform', translate(64), translate(0))
.addElement(document.querySelectorAll('ion-split-pane > ion-menu > ion-content')),
)
.addAnimation(
animationCtl
.create()
.fromTo('transform', translate(horizontal ? 32 : -72), translate(0))
.addElement(document.querySelectorAll('ion-router-outlet > .ion-page > ion-content')!),
)
.addAnimation(
animationCtl
.create()
.fromTo('transform', translate(100, '%'), translate(0, '%'))
.addElement(document.querySelector('stapps-navigation-tabs')!),
);
if (!horizontal) {
animation.addAnimation(
animationCtl
.create()
.fromTo('background', 'none', 'none')
.addElement(document.querySelector('ion-router-outlet')!),
);
const parallax = document
.querySelector('ion-router-outlet > .ion-page > ion-content')
?.shadowRoot?.querySelector('[part=parallax]');
if (parallax) {
animation.addAnimation(
animationCtl.create().fromTo('translate', '0 256px', '0 0px').addElement(parallax),
);
}
}
return animation;
}

View File

@@ -22,28 +22,16 @@ import {environment} from '../environments/environment';
import {Capacitor} from '@capacitor/core'; import {Capacitor} from '@capacitor/core';
import {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service'; import {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service';
import {Keyboard, KeyboardResize} from '@capacitor/keyboard'; import {Keyboard, KeyboardResize} from '@capacitor/keyboard';
import {AppVersionService} from './modules/about/app-version.service';
import {SplashScreen} from '@capacitor/splash-screen'; import {SplashScreen} from '@capacitor/splash-screen';
import {AppVersionService} from './modules/about/app-version.service';
/**
* TODO
*/
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: 'app.component.html', templateUrl: 'app.component.html',
}) })
export class AppComponent implements AfterContentInit { export class AppComponent implements AfterContentInit {
/**
* TODO
*/
pages: Array<{ pages: Array<{
/**
* TODO
*/
component: unknown; component: unknown;
/**
* TODO
*/
title: string; title: string;
}>; }>;
@@ -65,7 +53,7 @@ export class AppComponent implements AfterContentInit {
void this.initializeApp(); void this.initializeApp();
} }
async ngAfterContentInit() { ngAfterContentInit() {
this.scheduleSyncService.init(); this.scheduleSyncService.init();
void this.scheduleSyncService.enable(); void this.scheduleSyncService.enable();
this.versionService.getPendingReleaseNotes().then(notes => { this.versionService.getPendingReleaseNotes().then(notes => {
@@ -74,24 +62,11 @@ export class AppComponent implements AfterContentInit {
} }
}); });
if (document.readyState === 'complete') {
this.hideSplash();
} else {
document.addEventListener('readystatechange', () => {
if (document.readyState === 'complete') this.hideSplash();
});
}
}
async hideSplash() {
if (Capacitor.isNativePlatform()) { if (Capacitor.isNativePlatform()) {
void SplashScreen.hide(); void SplashScreen.hide();
} }
} }
/**
* TODO
*/
async initializeApp() { async initializeApp() {
App.addListener('appUrlOpen', (event: URLOpenListenerEvent) => { App.addListener('appUrlOpen', (event: URLOpenListenerEvent) => {
this.zone.run(() => { this.zone.run(() => {

View File

@@ -25,12 +25,10 @@ import moment from 'moment';
import 'moment/min/locales'; import 'moment/min/locales';
import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger'; import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger';
import SwiperCore, {FreeMode, Navigation} from 'swiper'; import SwiperCore, {FreeMode, Navigation} from 'swiper';
import {environment} from '../environments/environment'; import {environment} from '../environments/environment';
import {AppRoutingModule} from './app-routing.module'; import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component'; import {AppComponent} from './app.component';
import {CatalogModule} from './modules/catalog/catalog.module'; import {CatalogModule} from './modules/catalog/catalog.module';
import {ConfigModule} from './modules/config/config.module';
import {ConfigProvider} from './modules/config/config.provider'; import {ConfigProvider} from './modules/config/config.provider';
import {DashboardModule} from './modules/dashboard/dashboard.module'; import {DashboardModule} from './modules/dashboard/dashboard.module';
import {DataModule} from './modules/data/data.module'; import {DataModule} from './modules/data/data.module';
@@ -44,7 +42,6 @@ import {SettingsProvider} from './modules/settings/settings.provider';
import {StorageModule} from './modules/storage/storage.module'; import {StorageModule} from './modules/storage/storage.module';
import {ThingTranslateModule} from './translation/thing-translate.module'; import {ThingTranslateModule} from './translation/thing-translate.module';
import {UtilModule} from './util/util.module'; import {UtilModule} from './util/util.module';
import {initLogger} from './_helpers/ts-logger';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {AboutModule} from './modules/about/about.module'; import {AboutModule} from './modules/about/about.module';
import {JobModule} from './modules/jobs/jobs.module'; import {JobModule} from './modules/jobs/jobs.module';
@@ -91,28 +88,25 @@ export function initializerFactory(
) { ) {
return async () => { return async () => {
try { try {
initLogger(logger);
await storageProvider.init(); await storageProvider.init();
await configProvider.init(); await configProvider.init();
await settingsProvider.init(); if (configProvider.firstSession) {
// set language from browser
await settingsProvider.setSettingValue(
'profile',
'language',
translateService.getBrowserLang() as SCSettingValue,
);
}
const languageCode = await settingsProvider.getSetting<string>('profile', 'language');
// this language will be used as a fallback when a translation isn't found in the current language
translateService.setDefaultLang('en');
translateService.use(languageCode);
moment.locale(languageCode);
const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
setDefaultOptions({locale: dateFnsLocale});
dateFnsConfigurationService.setLocale(dateFnsLocale);
try { try {
if (configProvider.firstSession) {
// set language from browser
await settingsProvider.setSettingValue(
'profile',
'language',
translateService.getBrowserLang() as SCSettingValue,
);
}
const languageCode = (await settingsProvider.getValue('profile', 'language')) as string;
// this language will be used as a fallback when a translation isn't found in the current language
translateService.setDefaultLang('en');
translateService.use(languageCode);
moment.locale(languageCode);
const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
setDefaultOptions({locale: dateFnsLocale});
dateFnsConfigurationService.setLocale(dateFnsLocale);
await defaultAuthService.init(); await defaultAuthService.init();
await paiaAuthService.init(); await paiaAuthService.init();
} catch (error) { } catch (error) {
@@ -151,11 +145,12 @@ export function createTranslateLoader(http: HttpClient) {
BrowserAnimationsModule, BrowserAnimationsModule,
CatalogModule, CatalogModule,
CommonModule, CommonModule,
ConfigModule,
DashboardModule, DashboardModule,
DataModule, DataModule,
HebisModule, HebisModule,
IonicModule.forRoot(), IonicModule.forRoot({
animated: 'Cypress' in window ? false : undefined,
}),
IonIconModule, IonIconModule,
JobModule, JobModule,
FavoritesModule, FavoritesModule,

View File

@@ -1,17 +1,17 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
<ion-header> <ion-header>
<ion-toolbar color="primary" mode="ios"> <ion-toolbar color="primary" mode="ios">
@@ -24,24 +24,28 @@
</ion-header> </ion-header>
<ion-content parallax> <ion-content parallax>
<div class="licenses-content"> <div class="licenses-content">
@for (license of licenses; track license) { <ion-card
<ion-card [href]="license.url || license.repository" rel="external" target="_blank"> *ngFor="let license of licenses"
<ion-card-header> [href]="license.url || license.repository"
<ion-card-title> rel="external"
{{ license.name }} target="_blank"
<ion-icon [size]="16" [weight]="300" class="supertext-icon" name="open_in_browser"></ion-icon> >
</ion-card-title> <ion-card-header>
@if (license.authors || license.publisher) { <ion-card-title>
<ion-card-subtitle> {{ license.authors || license.publisher }} </ion-card-subtitle> {{ license.name }}
} <ion-icon [size]="16" [weight]="300" class="supertext-icon" name="open_in_browser"></ion-icon>
</ion-card-header> </ion-card-title>
<ion-card-content>
<ion-chip (click)="$event.preventDefault(); viewLicense(license)"> <ion-card-subtitle *ngIf="license.authors || license.publisher">
<ion-icon name="copyright"></ion-icon> {{ license.authors || license.publisher }}
<ion-label>{{ license.licenses }} License</ion-label> </ion-card-subtitle>
</ion-chip> </ion-card-header>
</ion-card-content> <ion-card-content>
</ion-card> <ion-chip (click)="$event.preventDefault(); viewLicense(license)">
} <ion-icon name="copyright"></ion-icon>
<ion-label>{{ license.licenses }} License</ion-label>
</ion-chip>
</ion-card-content>
</ion-card>
</div> </div>
</ion-content> </ion-content>

View File

@@ -1,56 +1,41 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
@if (content.type === 'markdown') { <markdown *ngIf="content.type === 'markdown'" [data]="'value' | translateSimple : content"></markdown>
<markdown [data]="'value' | translateSimple: content"></markdown> <div *ngIf="content.type ==='section'">
} <ion-card *ngIf="content.card; else noCard">
@if (content.type === 'section') { <ion-card-header>
<div> <ion-card-title>{{ 'title' | translateSimple : content }}</ion-card-title>
@if (content.card) { </ion-card-header>
<ion-card> <ion-card-content>
<ion-card-header>
<ion-card-title>{{ 'title' | translateSimple: content }}</ion-card-title>
</ion-card-header>
<ion-card-content>
<about-page-content [content]="content.content"></about-page-content>
</ion-card-content>
</ion-card>
} @else {
<h2>{{ 'title' | translateSimple: content }}</h2>
<about-page-content [content]="content.content"></about-page-content> <about-page-content [content]="content.content"></about-page-content>
} </ion-card-content>
</div> </ion-card>
} <ng-template #noCard>
@if (content.type === 'table') { <h2>{{ 'title' | translateSimple : content }}</h2>
<ion-grid> <about-page-content [content]="content.content"></about-page-content>
@for (row of content.rows; track row) { </ng-template>
<ion-row> </div>
@for (col of row; track col) { <ion-grid *ngIf="content.type === 'table'">
<ion-col> <ion-row *ngFor="let row of content.rows">
<about-page-content [content]="col"></about-page-content> <ion-col *ngFor="let col of row">
</ion-col> <about-page-content [content]="col"></about-page-content>
} </ion-col>
</ion-row> </ion-row>
} </ion-grid>
</ion-grid> <ion-item *ngIf="content.type === 'router link'" [routerLink]="content.link">
} <ion-icon *ngIf="content.icon" [name]="content.icon" slot="start"></ion-icon>
@if (content.type === 'router link') { <ion-label>{{ 'title' | translateSimple : content }}</ion-label>
<ion-item [routerLink]="content.link"> </ion-item>
@if (content.icon) {
<ion-icon [name]="content.icon" slot="start"></ion-icon>
}
<ion-label>{{ 'title' | translateSimple: content }}</ion-label>
</ion-item>
}

View File

@@ -14,7 +14,7 @@
*/ */
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router'; import {ActivatedRoute} from '@angular/router';
import {SCAboutPage, SCAppConfiguration} from '@openstapps/core'; import {SCAboutPage} from '@openstapps/core';
import {ConfigProvider} from '../../config/config.provider'; import {ConfigProvider} from '../../config/config.provider';
import packageJson from '../../../../../package.json'; import packageJson from '../../../../../package.json';
import config from 'capacitor.config'; import config from 'capacitor.config';
@@ -42,8 +42,7 @@ export class AboutPageComponent implements OnInit {
async ngOnInit() { async ngOnInit() {
const route = this.route.snapshot.url.map(it => it.path).join('/'); const route = this.route.snapshot.url.map(it => it.path).join('/');
this.content = this.content = this.configProvider.config.app.aboutPages[route] ?? {};
(this.configProvider.getValue('aboutPages') as SCAppConfiguration['aboutPages'])[route] ?? {};
this.version = Capacitor.getPlatform() === 'web' ? 'Web' : await App.getInfo().then(info => info.version); this.version = Capacitor.getPlatform() === 'web' ? 'Web' : await App.getInfo().then(info => info.version);
} }
} }

View File

@@ -1,37 +1,32 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
<ion-header> <ion-header>
<ion-toolbar color="primary" mode="ios"> <ion-toolbar color="primary" mode="ios">
<ion-buttons slot="start"> <ion-buttons slot="start">
<ion-back-button></ion-back-button> <ion-back-button></ion-back-button>
</ion-buttons> </ion-buttons>
@if (content) { <ion-title *ngIf="content; else titleLoading">{{ 'title' | translateSimple : content }}</ion-title>
<ion-title>{{ 'title' | translateSimple: content }}</ion-title> <ng-template #titleLoading>
} @else {
<ion-title><ion-skeleton-text animated="true"></ion-skeleton-text></ion-title> <ion-title><ion-skeleton-text animated="true"></ion-skeleton-text></ion-title>
} </ng-template>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
@if (content) { <ion-content parallax *ngIf="content">
<ion-content parallax> <ion-text>{{ 'about.VERSION_INFO' | translate: {name, version, stappsVersion} }}</ion-text>
<ion-text>{{ 'about.VERSION_INFO' | translate: {name, version, stappsVersion} }}</ion-text> <div class="page-content">
<div class="page-content"> <about-page-content *ngFor="let element of content.content" [content]="element"></about-page-content>
@for (element of content.content; track element) { </div>
<about-page-content [content]="element"></about-page-content> </ion-content>
}
</div>
</ion-content>
}

View File

@@ -19,7 +19,6 @@ import {FormsModule} from '@angular/forms';
import {IonicModule} from '@ionic/angular'; import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core'; import {TranslateModule} from '@ngx-translate/core';
import {ThingTranslateModule} from '../../translation/thing-translate.module'; import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {ConfigProvider} from '../config/config.provider';
import {AboutPageComponent} from './about-page/about-page.component'; import {AboutPageComponent} from './about-page/about-page.component';
import {MarkdownModule} from 'ngx-markdown'; import {MarkdownModule} from 'ngx-markdown';
import {AboutPageContentComponent} from './about-page/about-page-content.component'; import {AboutPageContentComponent} from './about-page/about-page-content.component';
@@ -64,6 +63,5 @@ const settingsRoutes: Routes = [
ScrollingModule, ScrollingModule,
UtilModule, UtilModule,
], ],
providers: [ConfigProvider],
}) })
export class AboutModule {} export class AboutModule {}

View File

@@ -5,6 +5,7 @@ import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {IonicModule, ModalController} from '@ionic/angular'; import {IonicModule, ModalController} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core'; import {TranslateModule} from '@ngx-translate/core';
import {UtilModule} from '../../util/util.module'; import {UtilModule} from '../../util/util.module';
import {CommonModule} from '@angular/common';
@Component({ @Component({
selector: 'stapps-release-notes', selector: 'stapps-release-notes',
@@ -12,7 +13,7 @@ import {UtilModule} from '../../util/util.module';
styleUrls: ['release-notes.scss'], styleUrls: ['release-notes.scss'],
standalone: true, standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [UtilModule, MarkdownModule, ThingTranslateModule, IonicModule, TranslateModule], imports: [UtilModule, MarkdownModule, ThingTranslateModule, IonicModule, TranslateModule, CommonModule],
}) })
export class ReleaseNotesComponent { export class ReleaseNotesComponent {
@Input() versionInfos: SCAppVersionInfo[]; @Input() versionInfos: SCAppVersionInfo[];

View File

@@ -1,20 +1,18 @@
<ion-header> <ion-header>
<ion-toolbar> <ion-toolbar>
<ion-title <ion-title><span class="ion-text-wrap">{{'releaseNotes.TITLE_UPDATED' | translate}}</span></ion-title>
><span class="ion-text-wrap">{{ 'releaseNotes.TITLE_UPDATED' | translate }}</span></ion-title
>
<ion-buttons slot="end"> <ion-buttons slot="end">
<ion-button [strong]="true" (click)="modalController.dismiss()">{{ <ion-button [strong]="true" (click)="modalController.dismiss()"
'modal.DISMISS_NEUTRAL' | translate >{{'modal.DISMISS_NEUTRAL' | translate}}</ion-button
}}</ion-button> >
</ion-buttons> </ion-buttons>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content parallax> <ion-content parallax>
@for (versionInfo of versionInfos; track versionInfo) { <ng-container *ngFor="let versionInfo of versionInfos">
<markdown <markdown
class="content-card ion-padding" class="content-card ion-padding"
[data]="'releaseNotes' | translateSimple: versionInfo" [data]="'releaseNotes' | translateSimple: versionInfo"
></markdown> ></markdown>
} </ng-container>
</ion-content> </ion-content>

View File

@@ -36,7 +36,7 @@
class="ion-text-wrap" class="ion-text-wrap"
*ngIf="assessments[key].courseOfStudy | async as course; else defaultLabel" *ngIf="assessments[key].courseOfStudy | async as course; else defaultLabel"
> >
{{ 'name' | thingTranslate: course }} ({{ 'academicDegree' | thingTranslate: course }}) {{ 'name' | thingTranslate : course }} ({{ 'academicDegree' | thingTranslate : course }})
</ion-label> </ion-label>
</div> </div>
<ng-template #defaultLabel> <ng-template #defaultLabel>

View File

@@ -14,12 +14,8 @@
--> -->
<ion-label [color]="passed ? undefined : 'danger'" <ion-label [color]="passed ? undefined : 'danger'"
>{{ >{{ (_item.grade | isNumeric) ? (_item.grade | numberLocalized :
(_item.grade | isNumeric) 'minimumFractionDigits:1,maximumFractionDigits:1') : '' }} {{ 'status' | thingTranslate : _item | titlecase
? (_item.grade | numberLocalized: 'minimumFractionDigits:1,maximumFractionDigits:1') }}, {{ 'attempt' | propertyNameTranslate : _item }} {{ _item.attempt }}
: ''
}}
{{ 'status' | thingTranslate: _item | titlecase }}, {{ 'attempt' | propertyNameTranslate: _item }}
{{ _item.attempt }}
</ion-label> </ion-label>
<ion-note> {{ _item.ects }} {{ 'ects' | propertyNameTranslate: _item }}</ion-note> <ion-note> {{ _item.ects }} {{ 'ects' | propertyNameTranslate : _item }}</ion-note>

View File

@@ -1,28 +1,24 @@
<!-- <!--
~ Copyright (C) 2022 StApps ~ Copyright (C) 2022 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
<ion-card> <ion-card>
<ion-card-content> <ion-card-content>
@if (item.courseOfStudy; as courseOfStudy) { <ion-note *ngIf="item.courseOfStudy as courseOfStudy">
<ion-note> {{ $any('courseOfStudy' | propertyNameTranslate : item) | titlecase }}: {{ 'name' | thingTranslate :
{{ $any('courseOfStudy' | propertyNameTranslate: item) | titlecase }}: $any(courseOfStudy) }} ({{ 'academicDegree' | thingTranslate : $any(courseOfStudy) }})
{{ 'name' | thingTranslate: $any(courseOfStudy) }} ({{ </ion-note>
'academicDegree' | thingTranslate: $any(courseOfStudy)
}})
</ion-note>
}
</ion-card-content> </ion-card-content>
</ion-card> </ion-card>
<ion-list class="container"> <ion-list class="container">

View File

@@ -14,6 +14,6 @@
--> -->
<div class="container"> <div class="container">
<h2 class="name">{{ 'name' | thingTranslate: item }} {{ item.date ? (item.date | amDateFormat) : '' }}</h2> <h2 class="name">{{ 'name' | thingTranslate : item }} {{ item.date ? (item.date | amDateFormat) : '' }}</h2>
<assessment-base-info [item]="item"></assessment-base-info> <assessment-base-info [item]="item"></assessment-base-info>
</div> </div>

View File

@@ -18,12 +18,7 @@ import {IPAIAAuthAction} from './paia/paia-auth-action';
import {AuthActions, IAuthAction} from 'ionic-appauth'; import {AuthActions, IAuthAction} from 'ionic-appauth';
import {TranslateService} from '@ngx-translate/core'; import {TranslateService} from '@ngx-translate/core';
import {JSONPath} from 'jsonpath-plus'; import {JSONPath} from 'jsonpath-plus';
import { import {SCAuthorizationProviderType, SCUserConfiguration} from '@openstapps/core';
SCAuthorizationProvider,
SCAuthorizationProviderType,
SCUserConfiguration,
SCUserConfigurationMap,
} from '@openstapps/core';
import {ConfigProvider} from '../config/config.provider'; import {ConfigProvider} from '../config/config.provider';
import {StorageProvider} from '../storage/storage.provider'; import {StorageProvider} from '../storage/storage.provider';
import {DefaultAuthService} from './default-auth.service'; import {DefaultAuthService} from './default-auth.service';
@@ -37,8 +32,6 @@ const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path';
providedIn: 'root', providedIn: 'root',
}) })
export class AuthHelperService { export class AuthHelperService {
userConfigurationMap: SCUserConfigurationMap;
constructor( constructor(
private translateService: TranslateService, private translateService: TranslateService,
private configProvider: ConfigProvider, private configProvider: ConfigProvider,
@@ -47,14 +40,7 @@ export class AuthHelperService {
private paiaAuth: PAIAAuthService, private paiaAuth: PAIAAuthService,
private browser: SimpleBrowser, private browser: SimpleBrowser,
private alertController: AlertController, private alertController: AlertController,
) { ) {}
this.userConfigurationMap =
(
this.configProvider.getAnyValue('auth') as {
default: SCAuthorizationProvider;
}
).default?.endpoints.mapping ?? {};
}
public getAuthMessage(provider: SCAuthorizationProviderType, action: IAuthAction | IPAIAAuthAction) { public getAuthMessage(provider: SCAuthorizationProviderType, action: IAuthAction | IPAIAAuthAction) {
let message: string | undefined; let message: string | undefined;
@@ -77,9 +63,10 @@ export class AuthHelperService {
name: '', name: '',
role: 'student', role: 'student',
}; };
for (const key in this.userConfigurationMap) { const mapping = this.configProvider.config.auth.default!.endpoints.mapping;
for (const key in mapping) {
user[key as keyof SCUserConfiguration] = JSONPath({ user[key as keyof SCUserConfiguration] = JSONPath({
path: this.userConfigurationMap[key as keyof SCUserConfiguration] as string, path: mapping[key as keyof SCUserConfiguration] as string,
json: userInfo, json: userInfo,
preventEval: true, preventEval: true,
})[0]; })[0];

View File

@@ -12,7 +12,6 @@
* 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 { import {
AuthorizationRequestHandler, AuthorizationRequestHandler,
AuthorizationServiceConfiguration, AuthorizationServiceConfiguration,
@@ -24,7 +23,6 @@ import {
} from '@openid/appauth'; } from '@openid/appauth';
import {AuthActionBuilder, Browser, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth'; import {AuthActionBuilder, Browser, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth';
import {ConfigProvider} from '../config/config.provider'; import {ConfigProvider} from '../config/config.provider';
import {SCAuthorizationProvider} from '@openstapps/core';
import {getClientConfig, getEndpointsConfig} from './auth.provider.methods'; import {getClientConfig, getEndpointsConfig} from './auth.provider.methods';
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {AuthService} from './auth.service'; import {AuthService} from './auth.service';
@@ -67,12 +65,9 @@ export class DefaultAuthService extends AuthService {
} }
setupConfiguration() { setupConfiguration() {
const authConfig = this.configProvider.getAnyValue('auth') as { this.authConfig = getClientConfig('default', this.configProvider.config.auth);
default: SCAuthorizationProvider;
};
this.authConfig = getClientConfig('default', authConfig);
this.localConfiguration = new AuthorizationServiceConfiguration( this.localConfiguration = new AuthorizationServiceConfiguration(
getEndpointsConfig('default', authConfig), getEndpointsConfig('default', this.configProvider.config.auth),
); );
} }

View File

@@ -12,7 +12,6 @@
* 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 { import {
AuthorizationError, AuthorizationError,
AuthorizationRequest, AuthorizationRequest,
@@ -47,7 +46,6 @@ import {PAIAAuthorizationResponse} from './paia-authorization-response';
import {PAIAAuthorizationNotifier} from './paia-authorization-notifier'; import {PAIAAuthorizationNotifier} from './paia-authorization-notifier';
import {PAIATokenResponse} from './paia-token-response'; import {PAIATokenResponse} from './paia-token-response';
import {IPAIAAuthAction, PAIAAuthActionBuilder} from './paia-auth-action'; import {IPAIAAuthAction, PAIAAuthActionBuilder} from './paia-auth-action';
import {SCAuthorizationProvider} from '@openstapps/core';
import {ConfigProvider} from '../../config/config.provider'; import {ConfigProvider} from '../../config/config.provider';
import {getClientConfig, getEndpointsConfig} from '../auth.provider.methods'; import {getClientConfig, getEndpointsConfig} from '../auth.provider.methods';
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
@@ -154,11 +152,10 @@ export class PAIAAuthService {
} }
setupConfiguration() { setupConfiguration() {
const authConfig = this.configProvider.getAnyValue('auth') as { this.authConfig = getClientConfig('paia', this.configProvider.config.auth);
paia: SCAuthorizationProvider; this.localConfiguration = new AuthorizationServiceConfiguration(
}; getEndpointsConfig('paia', this.configProvider.config.auth),
this.authConfig = getClientConfig('paia', authConfig); );
this.localConfiguration = new AuthorizationServiceConfiguration(getEndpointsConfig('paia', authConfig));
} }
protected notifyActionListers(action: IPAIAAuthAction) { protected notifyActionListers(action: IPAIAAuthAction) {

View File

@@ -1,17 +1,17 @@
<!-- <!--
~ Copyright (C) 2022 StApps ~ Copyright (C) 2022 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
<div> <div>
<ion-card-header> <ion-card-header>
<ion-card-title>{{ 'schedule.toCalendar.reviewModal.TITLE' | translate }}</ion-card-title> <ion-card-title>{{ 'schedule.toCalendar.reviewModal.TITLE' | translate }}</ion-card-title>
@@ -22,46 +22,34 @@
<ion-card-content> <ion-card-content>
<ion-list lines="none"> <ion-list lines="none">
@for (event of iCalEvents; track event) { <ion-item-group *ngFor="let event of iCalEvents">
<ion-item-group> <ion-item-divider>
<ion-item-divider> <ion-label>{{ event.title }}</ion-label>
<ion-label>{{ event.title }}</ion-label> <ion-note slot="start" *ngIf="event.events.length > 1">
@if (event.events.length > 1) { <ion-icon name="insert_page_break"></ion-icon>
<ion-note slot="start"> </ion-note>
<ion-icon name="insert_page_break"></ion-icon> </ion-item-divider>
</ion-note> <ion-item *ngFor="let iCalEvent of event.events">
} <ion-label>
</ion-item-divider> <s *ngIf="iCalEvent.cancelled; else date"
@for (iCalEvent of event.events; track iCalEvent) { ><ng-container [ngTemplateOutlet]="date"></ng-container>
<ion-item> </s>
<ion-label> <ng-template #date> {{ moment(iCalEvent.start) | amDateFormat : 'll, HH:mm' }} </ng-template>
@if (iCalEvent.cancelled) { </ion-label>
<s><ng-container [ngTemplateOutlet]="date"></ng-container> </s> <ion-note *ngIf="iCalEvent.rrule">
} @else { {{ iCalEvent.rrule.interval }} {{ iCalEvent.rrule.freq | sentencecase }}
{{ moment(iCalEvent.start) | amDateFormat: 'll, HH:mm' }} </ion-note>
} <ion-icon *ngIf="iCalEvent.rrule" name="event_repeat"></ion-icon>
<ng-template #date> {{ moment(iCalEvent.start) | amDateFormat: 'll, HH:mm' }} </ng-template> </ion-item>
</ion-label> </ion-item-group>
@if (iCalEvent.rrule) {
<ion-note>
{{ iCalEvent.rrule.interval }} {{ iCalEvent.rrule.freq | sentencecase }}
</ion-note>
}
@if (iCalEvent.rrule) {
<ion-icon name="event_repeat"></ion-icon>
}
</ion-item>
}
</ion-item-group>
}
</ion-list> </ion-list>
</ion-card-content> </ion-card-content>
<div class="horizontal-flex"> <div class="horizontal-flex">
<ion-item lines="none"> <ion-item lines="none">
<ion-checkbox [(ngModel)]="includeCancelled">{{ <ion-checkbox [(ngModel)]="includeCancelled"
'schedule.toCalendar.reviewModal.INCLUDE_CANCELLED' | translate >{{ 'schedule.toCalendar.reviewModal.INCLUDE_CANCELLED' | translate }}</ion-checkbox
}}</ion-checkbox> >
</ion-item> </ion-item>
</div> </div>
<div class="horizontal-flex"> <div class="horizontal-flex">
@@ -69,16 +57,15 @@
{{ 'share' | translate }} {{ 'share' | translate }}
<ion-icon slot="end" md="share" ios="ios_share"></ion-icon> <ion-icon slot="end" md="share" ios="ios_share"></ion-icon>
</ion-button> </ion-button>
@if (isWeb) { <ion-button fill="outline" (click)="download()" *ngIf="isWeb; else exportButton">
<ion-button fill="outline" (click)="download()"> {{ 'schedule.toCalendar.reviewModal.DOWNLOAD' | translate }}
{{ 'schedule.toCalendar.reviewModal.DOWNLOAD' | translate }} <ion-icon slot="end" name="download"></ion-icon>
<ion-icon slot="end" name="download"></ion-icon> </ion-button>
</ion-button> <ng-template #exportButton>
} @else {
<ion-button fill="outline" (click)="toCalendar()"> <ion-button fill="outline" (click)="toCalendar()">
{{ 'schedule.toCalendar.reviewModal.EXPORT' | translate }} {{ 'schedule.toCalendar.reviewModal.EXPORT' | translate }}
<ion-icon slot="end" name="event_upcoming"></ion-icon> <ion-icon slot="end" name="event_upcoming"></ion-icon>
</ion-button> </ion-button>
} </ng-template>
</div> </div>
</div> </div>

View File

@@ -12,7 +12,6 @@
* 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 {Calendar} from '@awesome-cordova-plugins/calendar/ngx'; import {Calendar} from '@awesome-cordova-plugins/calendar/ngx';
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {ICalEvent} from './ical/ical'; import {ICalEvent} from './ical/ical';
@@ -35,14 +34,14 @@ export class CalendarService {
goToDateClicked = this.goToDate.asObservable(); goToDateClicked = this.goToDate.asObservable();
calendarName = 'StApps'; calendarName: string;
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
constructor( constructor(
readonly calendar: Calendar, readonly calendar: Calendar,
private readonly configProvider: ConfigProvider, private readonly configProvider: ConfigProvider,
) { ) {
this.calendarName = (this.configProvider.getValue('name') as string) ?? 'StApps'; this.calendarName = this.configProvider.config.app.name ?? 'StApps';
} }
async createCalendar(): Promise<CalendarInfo | undefined> { async createCalendar(): Promise<CalendarInfo | undefined> {

View File

@@ -1,17 +1,17 @@
<!-- <!--
~ Copyright (C) 2022 StApps ~ Copyright (C) 2022 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
<ion-header> <ion-header>
<ion-toolbar color="primary" mode="ios"> <ion-toolbar color="primary" mode="ios">
@@ -23,45 +23,33 @@
</ion-toolbar> </ion-toolbar>
<ion-toolbar color="primary" mode="md"> <ion-toolbar color="primary" mode="md">
<ion-segment (ionChange)="segmentChanged($event)" [value]="selectedSemesterUID" mode="md"> <ion-segment (ionChange)="segmentChanged($event)" [value]="selectedSemesterUID" mode="md">
@for (semester of availableSemesters; track semester) { <ion-segment-button *ngFor="let semester of availableSemesters" [value]="semester.uid">
<ion-segment-button [value]="semester.uid"> <ion-label>{{ semester.acronym }}</ion-label>
<ion-label>{{ semester.acronym }}</ion-label> </ion-segment-button>
</ion-segment-button>
}
</ion-segment> </ion-segment>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
@if (catalogs && catalogs.length > 0) { <ion-list *ngIf="catalogs && catalogs.length > 0">
<ion-list> <ion-item *ngFor="let catalog of catalogs" button="true" lines="inset" (click)="notifySelect(catalog)">
@for (catalog of catalogs; track catalog) { <ion-label>
<ion-item button="true" lines="inset" (click)="notifySelect(catalog)"> <h2>{{ catalog.name }}</h2>
</ion-label>
</ion-item>
</ion-list>
<ion-list *ngIf="!catalogs">
<stapps-skeleton-list-item *ngFor="let skeleton of [].constructor(10)"> </stapps-skeleton-list-item>
</ion-list>
<ion-grid *ngIf="catalogs && catalogs.length === 0">
<ion-row>
<ion-col>
<div class="ion-text-center margin-top">
<ion-label> <ion-label>
<h2>{{ catalog.name }}</h2> {{ 'catalog.detail.EMPTY_SEMESTER' | translate }}
</ion-label> </ion-label>
</ion-item> </div>
} </ion-col>
</ion-list> </ion-row>
} </ion-grid>
@if (!catalogs) {
<ion-list>
@for (skeleton of [].constructor(10); track skeleton) {
<stapps-skeleton-list-item> </stapps-skeleton-list-item>
}
</ion-list>
}
@if (catalogs && catalogs.length === 0) {
<ion-grid>
<ion-row>
<ion-col>
<div class="ion-text-center margin-top">
<ion-label>
{{ 'catalog.detail.EMPTY_SEMESTER' | translate }}
</ion-label>
</div>
</ion-col>
</ion-row>
</ion-grid>
}
</ion-content> </ion-content>

View File

@@ -20,7 +20,6 @@ import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core'; import {TranslateModule} from '@ngx-translate/core';
import {MomentModule} from 'ngx-moment'; import {MomentModule} from 'ngx-moment';
import {DataModule} from '../data/data.module'; import {DataModule} from '../data/data.module';
import {SettingsProvider} from '../settings/settings.provider';
import {CatalogComponent} from './catalog.component'; import {CatalogComponent} from './catalog.component';
import {UtilModule} from '../../util/util.module'; import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module'; import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
@@ -46,6 +45,5 @@ const catalogRoutes: Routes = [
DataModule, DataModule,
UtilModule, UtilModule,
], ],
providers: [SettingsProvider],
}) })
export class CatalogModule {} export class CatalogModule {}

View File

@@ -16,12 +16,6 @@ import {TestBed} from '@angular/core/testing';
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider'; import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
import {StorageProvider} from '../storage/storage.provider'; import {StorageProvider} from '../storage/storage.provider';
import {ConfigProvider, STORAGE_KEY_CONFIG} from './config.provider'; import {ConfigProvider, STORAGE_KEY_CONFIG} from './config.provider';
import {
ConfigFetchError,
ConfigInitError,
SavedConfigNotAvailable,
WrongConfigVersionInStorage,
} from './errors';
import {NGXLogger} from 'ngx-logger'; import {NGXLogger} from 'ngx-logger';
import {sampleIndexResponse} from '../../_helpers/data/sample-configuration'; import {sampleIndexResponse} from '../../_helpers/data/sample-configuration';

View File

@@ -14,19 +14,14 @@
*/ */
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {Client} from '@openstapps/api'; import {Client} from '@openstapps/api';
import {SCAppConfiguration, SCIndexResponse} from '@openstapps/core'; import {SCIndexResponse} from '@openstapps/core';
import {CORE_VERSION} from '@openstapps/core'; import packageInfo from '@openstapps/core/package.json';
import {NGXLogger} from 'ngx-logger'; import {NGXLogger} from 'ngx-logger';
import {environment} from '../../../environments/environment'; import {environment} from '../../../environments/environment';
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider'; import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
import {StorageProvider} from '../storage/storage.provider'; import {StorageProvider} from '../storage/storage.provider';
import { import equals from 'fast-deep-equal/es6';
ConfigFetchError, import {BehaviorSubject} from 'rxjs';
ConfigInitError,
ConfigValueNotAvailable,
SavedConfigNotAvailable,
WrongConfigVersionInStorage,
} from './errors';
/** /**
* Key to store config in storage module * Key to store config in storage module
@@ -35,6 +30,17 @@ import {
*/ */
export const STORAGE_KEY_CONFIG = 'stapps.config'; export const STORAGE_KEY_CONFIG = 'stapps.config';
/**
* Makes an object deeply immutable
*/
function deepFreeze<T extends object>(object: T) {
for (const key of Object.keys(object)) {
const value = (object as Record<string, unknown>)[key];
if (typeof value === 'object' && !Object.isFrozen(value)) deepFreeze(value!);
}
return Object.freeze(object);
}
/** /**
* Provides configuration * Provides configuration
*/ */
@@ -50,18 +56,23 @@ export class ConfigProvider {
/** /**
* App configuration as IndexResponse * App configuration as IndexResponse
*/ */
config: SCIndexResponse; config: Readonly<SCIndexResponse>;
/** /**
* Version of the @openstapps/core package that app is using * Version of the @openstapps/core package that app is using
*/ */
scVersion = CORE_VERSION; scVersion = packageInfo.version;
/** /**
* First session indicator (config not found in storage) * First session indicator (config not found in storage)
*/ */
firstSession = true; firstSession = true;
/**
* If the config requires an update
*/
needsUpdate$ = new BehaviorSubject(false);
/** /**
* Constructor, initialise api client * Constructor, initialise api client
* @param storageProvider StorageProvider to load persistent configuration * @param storageProvider StorageProvider to load persistent configuration
@@ -76,104 +87,35 @@ export class ConfigProvider {
this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version); this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version);
} }
/**
* Fetches configuration from backend
*/
async fetch(): Promise<SCIndexResponse> {
try {
return await this.client.handshake(this.scVersion);
} catch {
throw new ConfigFetchError();
}
}
/**
* Returns the value of an app configuration
* @param attribute requested attribute from app configuration
*/
public getValue(attribute: keyof SCAppConfiguration) {
if (this.config.app[attribute] !== undefined) {
return this.config.app[attribute];
}
throw new ConfigValueNotAvailable(attribute);
}
/**
* Returns a value of the configuration (not only app configuration)
* @param attribute requested attribute from the configuration
*/
public getAnyValue(attribute: keyof SCIndexResponse) {
if (this.config[attribute] !== undefined) {
return this.config[attribute];
}
throw new ConfigValueNotAvailable(attribute);
}
/** /**
* Initialises the ConfigProvider * Initialises the ConfigProvider
* @throws ConfigInitError if no configuration could be loaded.
* @throws WrongConfigVersionInStorage if fetch failed and saved config has wrong SCVersion
*/ */
async init(): Promise<void> { async init(): Promise<void> {
let loadError; this.config = (await this.storageProvider.has(STORAGE_KEY_CONFIG))
let fetchError; ? await this.storageProvider.get<SCIndexResponse>(STORAGE_KEY_CONFIG)
// load saved configuration : undefined!;
try { this.firstSession = !this.config;
this.config = await this.loadLocal();
this.firstSession = false; const updatedConfig = this.client.handshake(this.scVersion).then(async fetchedConfig => {
this.logger.log(`initialised configuration from storage`); if (!equals(fetchedConfig, this.config)) {
if (this.config.backend.SCVersion !== this.scVersion) { await this.storageProvider.put(STORAGE_KEY_CONFIG, fetchedConfig);
loadError = new WrongConfigVersionInStorage(this.scVersion, this.config.backend.SCVersion); this.logger.log(`Config updated`);
this.needsUpdate$.next(true);
this.needsUpdate$.complete();
} }
} catch (error) { return fetchedConfig;
loadError = error; });
}
// fetch remote configuration from backend
try {
const fetchedConfig: SCIndexResponse = await this.fetch();
await this.set(fetchedConfig);
this.logger.log(`initialised configuration from remote`);
} catch (error) {
fetchError = error;
}
// check for occurred errors and throw them
if (loadError !== undefined && fetchError !== undefined) {
throw new ConfigInitError();
}
if (loadError !== undefined) {
this.logger.warn(loadError);
}
if (fetchError !== undefined) {
this.logger.warn(fetchError);
}
}
/** this.config ??= await updatedConfig;
* Returns saved configuration from StorageModule this.config = deepFreeze(this.config);
* @throws SavedConfigNotAvailable if no configuration could be loaded
*/ if (this.config.backend.SCVersion !== this.scVersion) {
async loadLocal(): Promise<SCIndexResponse> { this.logger.warn(
// get local configuration 'Incompatible config, expected',
if (await this.storageProvider.has(STORAGE_KEY_CONFIG)) { this.scVersion,
return this.storageProvider.get<SCIndexResponse>(STORAGE_KEY_CONFIG); 'but got',
this.config.backend.SCVersion,
);
} }
throw new SavedConfigNotAvailable();
}
/**
* Saves the configuration from the provider
* @param config configuration to save
*/
async save(config: SCIndexResponse): Promise<void> {
await this.storageProvider.put(STORAGE_KEY_CONFIG, config);
}
/**
* Sets the configuration in the module and writes it into app storage
* @param config SCIndexResponse to set
*/
async set(config: SCIndexResponse): Promise<void> {
this.config = config;
await this.save(this.config);
} }
} }

View File

@@ -1,65 +0,0 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {AppError} from '../../_helpers/errors';
/**
* Error that is thrown when fetching from backend fails
*/
export class ConfigFetchError extends AppError {
constructor() {
super('ConfigFetchError', 'App configuration could not be fetched!');
}
}
/**
* Error that is thrown when the ConfigProvider could be initialised
*/
export class ConfigInitError extends AppError {
constructor() {
super('ConfigInitError', 'App configuration could not be initialised!');
}
}
/**
* Error that is thrown when the requested config value is not available
*/
export class ConfigValueNotAvailable extends AppError {
constructor(valueKey: string) {
super('ConfigValueNotAvailable', `No attribute "${valueKey}" in config available!`);
}
}
/**
* Error that is thrown when no saved config is available
*/
export class SavedConfigNotAvailable extends AppError {
constructor() {
super('SavedConfigNotAvailable', 'No saved app configuration available.');
}
}
/**
* Error that is thrown when the SCVersion of the saved config is not compatible with the app
*/
export class WrongConfigVersionInStorage extends AppError {
constructor(correctVersion: string, savedVersion: string) {
super(
'WrongConfigVersionInStorage',
`The saved configs backend version ${savedVersion} ` +
`does not equal the configured backend version ${correctVersion} of the app.`,
);
}
}

View File

@@ -21,7 +21,6 @@ import {SwiperModule} from 'swiper/angular';
import {TranslateModule, TranslatePipe} from '@ngx-translate/core'; import {TranslateModule, TranslatePipe} from '@ngx-translate/core';
import {MomentModule} from 'ngx-moment'; import {MomentModule} from 'ngx-moment';
import {DataModule} from '../data/data.module'; import {DataModule} from '../data/data.module';
import {SettingsProvider} from '../settings/settings.provider';
import {DashboardComponent} from './dashboard.component'; import {DashboardComponent} from './dashboard.component';
import {SearchSectionComponent} from './sections/search-section/search-section.component'; import {SearchSectionComponent} from './sections/search-section/search-section.component';
import {NewsSectionComponent} from './sections/news-section/news-section.component'; import {NewsSectionComponent} from './sections/news-section/news-section.component';
@@ -70,6 +69,6 @@ const catalogRoutes: Routes = [
NewsModule, NewsModule,
JobModule, JobModule,
], ],
providers: [SettingsProvider, TranslatePipe], providers: [TranslatePipe],
}) })
export class DashboardModule {} export class DashboardModule {}

View File

@@ -1,34 +1,32 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
<stapps-section [title]="'dashboard.favorites.title' | translate"> <stapps-section [title]="'dashboard.favorites.title' | translate">
<ion-button slot="button-end" fill="clear" color="medium" [routerLink]="['/favorites']"> <ion-button slot="button-end" fill="clear" color="medium" [routerLink]="['/favorites']">
<ion-icon slot="icon-only" name="search" [size]="24"></ion-icon> <ion-icon slot="icon-only" name="search" [size]="24"></ion-icon>
</ion-button> </ion-button>
@if (items | async; as items) { <simple-swiper *ngIf="items | async as items; else noItems" @fade>
<simple-swiper @fade> <stapps-data-list-item
@for (item of items; track item) { *ngFor="let item of items"
<stapps-data-list-item [hideThumbnail]="true"
[hideThumbnail]="true" [listItemEndInteraction]="false"
[listItemEndInteraction]="false" [item]="item"
[item]="item" appearance="square"
appearance="square" ></stapps-data-list-item>
></stapps-data-list-item> </simple-swiper>
} <ng-template #noItems>
</simple-swiper>
} @else {
<ion-item class="nothing-selected" lines="none"> <ion-item class="nothing-selected" lines="none">
<ion-label class="ion-text-wrap"> <ion-label class="ion-text-wrap">
{{ 'dashboard.favorites.no_favorite_prefix' | translate }} {{ 'dashboard.favorites.no_favorite_prefix' | translate }}
@@ -36,5 +34,5 @@
{{ 'dashboard.favorites.no_favorite_suffix' | translate }} {{ 'dashboard.favorites.no_favorite_suffix' | translate }}
</ion-label> </ion-label>
</ion-item> </ion-item>
} </ng-template>
</stapps-section> </stapps-section>

View File

@@ -1,44 +1,43 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
<stapps-section [title]="'dashboard.jobs.title' | translate"> <stapps-section [title]="'dashboard.jobs.title' | translate">
<ion-button slot="button-end" fill="clear" color="medium" [routerLink]="['/jobs']"> <ion-button slot="button-end" fill="clear" color="medium" [routerLink]="['/jobs']">
<ion-icon slot="icon-only" name="search" [size]="24"></ion-icon> <ion-icon slot="icon-only" name="search" [size]="24"></ion-icon>
</ion-button> </ion-button>
@if (jobs | async; as jobs) { <simple-swiper *ngIf="jobs | async as jobs; else noItems" @fade>
<simple-swiper @fade> <stapps-data-list-item
@for (item of jobs; track item) { *ngFor="let item of jobs"
<stapps-data-list-item [hideThumbnail]="true"
[hideThumbnail]="true" [item]="item"
[item]="item" appearance="square"
appearance="square" ></stapps-data-list-item>
></stapps-data-list-item> <ion-item [routerLink]="['/jobs']" class="more-jobs" lines="none">
} <ion-label>{{ 'dashboard.jobs.title' | translate | titlecase }}</ion-label>
<ion-item [routerLink]="['/jobs']" class="more-jobs" lines="none"> <ion-icon color="medium" name="read_more" [size]="40"></ion-icon>
<ion-label>{{ 'dashboard.jobs.title' | translate | titlecase }}</ion-label> </ion-item>
<ion-icon color="medium" name="read_more" [size]="40"></ion-icon> </simple-swiper>
</ion-item> <ng-template #noItems>
</simple-swiper>
} @else {
<ion-item class="nothing-selected" lines="none"> <ion-item class="nothing-selected" lines="none">
<ion-label class="ion-text-wrap"> <ion-label class="ion-text-wrap">
{{ 'dashboard.jobs.noJobs' | translate }} {{ 'dashboard.jobs.noJobs' | translate }}
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-button slot="button-end" fill="clear" color="medium" [routerLink]="['/jobs']"> <ion-button slot="button-end" fill="clear" color="medium" [routerLink]="['/jobs']">
<ion-icon slot="icon-only" name="search" [size]="24"></ion-icon> <ion-icon slot="icon-only" name="search" [size]="24"></ion-icon>
</ion-button> </ion-button>
} </ng-template>
</stapps-section> </stapps-section>

View File

@@ -1,37 +1,33 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
@if (dishes | async; as dishes) { <ng-container *ngIf="dishes | async as dishes; else loading">
@if (dishes.length > 0) { <simple-swiper *ngIf="dishes.length > 0" @fade>
<simple-swiper @fade> <stapps-data-list-item
@for (dish of dishes; track dish) { *ngFor="let dish of dishes"
<stapps-data-list-item [hideThumbnail]="true"
[hideThumbnail]="true" [item]="dish"
[item]="dish" appearance="square"
appearance="square" ></stapps-data-list-item>
></stapps-data-list-item> </simple-swiper>
} <ion-item class="no-dishes" *ngIf="!dishes || dishes.length === 0" lines="none">
</simple-swiper> <ion-label>
} {{ 'dashboard.canteens.no_dishes_available' | translate }}
@if (!dishes || dishes.length === 0) { </ion-label>
<ion-item class="no-dishes" lines="none"> </ion-item>
<ion-label> </ng-container>
{{ 'dashboard.canteens.no_dishes_available' | translate }} <ng-template #loading>
</ion-label>
</ion-item>
}
} @else {
<div class="placeholder"></div> <div class="placeholder"></div>
} </ng-template>

View File

@@ -1,21 +1,21 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
@if (items | async; as items) { <ng-container *ngIf="items | async as items">
@if (items.length !== 0) { <ng-container *ngIf="items.length !== 0; else nothingSelected">
@for (item of items; track item) { <ng-container *ngFor="let item of items">
<stapps-section @fade [item]="item" [title]="'name' | thingTranslate: item"> <stapps-section @fade [item]="item" [title]="'name' | thingTranslate: item">
<ion-button slot="button-end" fill="clear" color="medium" (click)="favoritesService.delete(item)"> <ion-button slot="button-end" fill="clear" color="medium" (click)="favoritesService.delete(item)">
<ion-icon slot="icon-only" name="delete" [size]="24"></ion-icon> <ion-icon slot="icon-only" name="delete" [size]="24"></ion-icon>
@@ -23,8 +23,9 @@
<stapps-opening-hours slot="subtitle" [openingHours]="$any(item).openingHours"></stapps-opening-hours> <stapps-opening-hours slot="subtitle" [openingHours]="$any(item).openingHours"></stapps-opening-hours>
<stapps-mensa-section-content [item]="item"></stapps-mensa-section-content> <stapps-mensa-section-content [item]="item"></stapps-mensa-section-content>
</stapps-section> </stapps-section>
} </ng-container>
} @else { </ng-container>
<ng-template #nothingSelected>
<stapps-section [title]="'dashboard.canteens.title' | translate"> <stapps-section [title]="'dashboard.canteens.title' | translate">
<ion-item class="nothing-selected" lines="none"> <ion-item class="nothing-selected" lines="none">
<ion-label class="ion-text-wrap"> <ion-label class="ion-text-wrap">
@@ -34,5 +35,5 @@
</ion-label> </ion-label>
</ion-item> </ion-item>
</stapps-section> </stapps-section>
} </ng-template>
} </ng-container>

View File

@@ -1,33 +1,29 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
<stapps-section [title]="'dashboard.news.title' | translate"> <stapps-section [title]="'dashboard.news.title' | translate">
<ion-button size="small" slot="button-end" fill="clear" color="medium" [routerLink]="['/news']"> <ion-button size="small" slot="button-end" fill="clear" color="medium" [routerLink]="['/news']">
<ion-icon slot="icon-only" name="read_more"></ion-icon> <ion-icon slot="icon-only" name="read_more"></ion-icon>
</ion-button> </ion-button>
@if (news | async; as news) { <simple-swiper class="news-swiper card-swiper" *ngIf="news | async as news" @fade>
<simple-swiper class="news-swiper card-swiper" @fade> <stapps-news-item *ngFor="let newsItem of news" [item]="newsItem"> </stapps-news-item>
@for (newsItem of news; track newsItem) { <ion-item [routerLink]="['/news']" class="more-news" lines="none">
<stapps-news-item [item]="newsItem"> </stapps-news-item> <ion-label>{{ 'dashboard.news.moreNews' | translate | titlecase }}</ion-label>
} <ion-thumbnail class="ion-margin-end">
<ion-item [routerLink]="['/news']" class="more-news" lines="none"> <ion-icon color="medium" name="read_more" [size]="150"></ion-icon>
<ion-label>{{ 'dashboard.news.moreNews' | translate | titlecase }}</ion-label> </ion-thumbnail>
<ion-thumbnail class="ion-margin-end"> </ion-item>
<ion-icon color="medium" name="read_more" [size]="150"></ion-icon> </simple-swiper>
</ion-thumbnail>
</ion-item>
</simple-swiper>
}
</stapps-section> </stapps-section>

View File

@@ -63,10 +63,10 @@ simple-swiper {
.title { .title {
overflow: hidden; overflow: hidden;
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical;
white-space: break-spaces; white-space: break-spaces;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3; -webkit-line-clamp: 3;
} }

View File

@@ -14,7 +14,7 @@
*/ */
import {AnimationController} from '@ionic/angular'; import {AnimationController} from '@ionic/angular';
import {AnimationOptions} from '@ionic/angular/common/providers/nav-controller'; import {AnimationOptions} from '@ionic/angular/providers/nav-controller';
/** /**
* *

View File

@@ -1,25 +1,19 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
@if (applicable.locate) { <stapps-locate-action-chip *ngIf="applicable.locate" [item]="item"></stapps-locate-action-chip>
<stapps-locate-action-chip [item]="item"></stapps-locate-action-chip> <stapps-navigate-action-chip *ngIf="applicable.navigate" [item]="$any(item)"></stapps-navigate-action-chip>
}
@if (applicable.navigate) {
<stapps-navigate-action-chip [item]="$any(item)"></stapps-navigate-action-chip>
}
<!-- Add Event Chip needs to load data and should be the last --> <!-- Add Event Chip needs to load data and should be the last -->
@if (applicable.event) { <stapps-add-event-action-chip *ngIf="applicable.event" [item]="item"></stapps-add-event-action-chip>
<stapps-add-event-action-chip [item]="item"></stapps-add-event-action-chip>
}

View File

@@ -1,57 +1,57 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
<div class="stack-children"> <div class="stack-children">
@if (associatedDateSeries | async; as associatedDateSeries) { <ion-chip
<ion-chip *ngIf="associatedDateSeries | async as associatedDateSeries; else loading"
@chipTransition @chipTransition
[disabled]="disabled" [disabled]="disabled"
(click)="$event.stopPropagation(); editModal.present()" (click)="$event.stopPropagation(); editModal.present()"
[color]="color" [color]="color"
[outline]="true" [outline]="true"
> >
<ion-icon [name]="icon" [fill]="iconFill"></ion-icon> <ion-icon [name]="icon" [fill]="iconFill"></ion-icon>
<ion-label>{{ label | translate }}</ion-label> <ion-label>{{ label | translate }}</ion-label>
<stapps-edit-modal #editModal (save)="selection.save()"> <stapps-edit-modal #editModal (save)="selection.save()">
<ng-template> <ng-template>
<ion-content parallax [parallaxSize]="160" class="ion-padding modal-content"> <ion-content parallax [parallaxSize]="160" class="ion-padding modal-content">
<stapps-edit-event-selection <stapps-edit-event-selection
#selection #selection
[items]="associatedDateSeries" [items]="associatedDateSeries"
(modified)="editModal.pendingChanges = true" (modified)="editModal.pendingChanges = true"
></stapps-edit-event-selection> ></stapps-edit-event-selection>
</ion-content> </ion-content>
<ion-footer mode="ios"> <ion-footer mode="ios">
<ion-toolbar color="light"> <ion-toolbar color="light">
<ion-button <ion-button
slot="end" slot="end"
fill="clear" fill="clear"
(click)="export()" (click)="export()"
[disabled]="!(selection.selection.indeterminate || selection.selection.checked)" [disabled]="!(selection.selection.indeterminate || selection.selection.checked)"
> >
{{ 'schedule.toCalendar.reviewModal.DOWNLOAD' | translate | titlecase }} {{ 'schedule.toCalendar.reviewModal.DOWNLOAD' | translate | titlecase }}
<ion-icon slot="end" name="download"></ion-icon> <ion-icon slot="end" name="download"></ion-icon>
</ion-button> </ion-button>
</ion-toolbar> </ion-toolbar>
</ion-footer> </ion-footer>
</ng-template> </ng-template>
</stapps-edit-modal> </stapps-edit-modal>
</ion-chip> </ion-chip>
} @else { <ng-template #loading>
<ion-chip @chipSkeletonTransition> <ion-chip @chipSkeletonTransition>
<ion-skeleton-text animated="true"></ion-skeleton-text> <ion-skeleton-text animated="true"></ion-skeleton-text>
</ion-chip> </ion-chip>
} </ng-template>
</div> </div>

View File

@@ -23,7 +23,8 @@
.stack-children { .stack-children {
display: grid; display: grid;
place-items: start start; align-items: start;
justify-items: start;
} }
.stack-children > * { .stack-children > * {

View File

@@ -14,5 +14,5 @@
--> -->
<ion-chip [color]="'primary'" [outline]="true" [geoNavigation]="place"> <ion-chip [color]="'primary'" [outline]="true" [geoNavigation]="place">
<ion-icon name="directions"></ion-icon> <ion-icon name="directions"></ion-icon>
<ion-label>{{ 'map.directions.TITLE' | translate }}</ion-label> <ion-label>{{'map.directions.TITLE' | translate}}</ion-label>
</ion-chip> </ion-chip>

View File

@@ -1,17 +1,17 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
<ion-item class="list-header" lines="none"> <ion-item class="list-header" lines="none">
<ion-checkbox <ion-checkbox
@@ -22,7 +22,7 @@
<ion-list-header> {{ 'data.chips.add_events.popover.ALL' | translate }} </ion-list-header> <ion-list-header> {{ 'data.chips.add_events.popover.ALL' | translate }} </ion-list-header>
</ion-checkbox> </ion-checkbox>
</ion-item> </ion-item>
@for (frequency of selection.children; track frequency) { <ng-container *ngFor="let frequency of selection.children">
<ion-list inset="true" lines="full"> <ion-list inset="true" lines="full">
<ion-item lines="none" class="list-header"> <ion-item lines="none" class="list-header">
<ion-checkbox <ion-checkbox
@@ -31,55 +31,48 @@
(ionChange)="modified.emit(); frequency.click()" (ionChange)="modified.emit(); frequency.click()"
> >
<ion-list-header> <ion-list-header>
{{ {{ frequency.children[0].item.repeatFrequency ? (frequency.children[0].item.repeatFrequency |
frequency.children[0].item.repeatFrequency durationLocalized: true | sentencecase) : ('data.chips.add_events.popover.SINGLE' | translate |
? (frequency.children[0].item.repeatFrequency | durationLocalized: true | sentencecase) titlecase) }}
: ('data.chips.add_events.popover.SINGLE' | translate | titlecase)
}}
</ion-list-header> </ion-list-header>
</ion-checkbox> </ion-checkbox>
</ion-item> </ion-item>
@for (date of frequency.children; track date) { <ion-item *ngFor="let date of frequency.children">
<ion-item> <ion-checkbox
<ion-checkbox [checked]="date.selected"
[checked]="date.selected" (ionChange)="modified.emit(); date.selected = !date.selected; frequency.notifyChildChanged()"
(ionChange)="modified.emit(); date.selected = !date.selected; frequency.notifyChildChanged()" >
> <ng-container *ngIf="date.item.dates.length > 1; else single_event">
@if (date.item.dates.length > 1) { <ion-text>
<ion-text> {{ date.item.dates[0] | amDateFormat: 'dddd, LT' }} - {{ date.item.dates[0] | amAdd:
{{ date.item.dates[0] | amDateFormat: 'dddd, LT' }} - date.item.duration | amDateFormat: 'LT' }}
{{ date.item.dates[0] | amAdd: date.item.duration | amDateFormat: 'LT' }} </ion-text>
</ion-text> <br />
<ion-text>
{{ date.item.dates[0] | amDateFormat: 'LL' }} - {{ date.item.dates[date.item.dates.length - 1] |
amDateFormat: 'LL' }}
</ion-text>
</ng-container>
<ng-template #single_event>
<ion-text *ngIf="date.item.dates[0] as time; else noDates">
{{ time | amDateFormat: 'LL, LT' }} - {{ time | amAdd: date.item.duration | amDateFormat: 'LT' }}
</ion-text>
<ng-template #noDates>
<ion-text color="danger">{{ 'data.chips.add_events.popover.DATA_ERROR' | translate }}</ion-text>
<br /> <br />
<ion-text> <ion-text *ngFor="let id of date.item.identifiers | keyvalue">
{{ date.item.dates[0] | amDateFormat: 'LL' }} - {{ id.key }}: {{ id.value }}
{{ date.item.dates[date.item.dates.length - 1] | amDateFormat: 'LL' }}
</ion-text> </ion-text>
} @else { </ng-template>
@if (date.item.dates[0]; as time) { </ng-template>
<ion-text> <ng-container class="ion-align-items-center" *ngIf="date.item.inPlace">
{{ time | amDateFormat: 'LL, LT' }} - <br />
{{ time | amAdd: date.item.duration | amDateFormat: 'LT' }} <ion-text color="medium" class="place">
</ion-text> <ion-icon name="pin_drop"></ion-icon>
} @else { <span> {{ 'inPlace.name' | thingTranslate: date.item }}</span>
<ion-text color="danger">{{ 'data.chips.add_events.popover.DATA_ERROR' | translate }}</ion-text> </ion-text>
<br /> </ng-container>
@for (id of date.item.identifiers | keyvalue; track id) { </ion-checkbox>
<ion-text> {{ id.key }}: {{ id.value }} </ion-text> </ion-item>
}
}
}
@if (date.item.inPlace) {
<ng-container class="ion-align-items-center">
<br />
<ion-text color="medium" class="place">
<ion-icon name="pin_drop"></ion-icon>
<span> {{ 'inPlace.name' | thingTranslate: date.item }}</span>
</ion-text>
</ng-container>
}
</ion-checkbox>
</ion-item>
}
</ion-list> </ion-list>
} </ng-container>

View File

@@ -1,23 +1,19 @@
<!-- <!--
~ Copyright (C) 2022 StApps ~ Copyright (C) 2022 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
@if (displayValue) { <ion-chip *ngIf="displayValue" [class.active]="active" (click)="emitToggle(value)">
<ion-chip [class.active]="active" (click)="emitToggle(value)"> <ion-icon class="ion-color" name="check_circle" [fill]="true" *ngIf="active"></ion-icon>
@if (active) { <ion-label>{{ displayValue }}</ion-label>
<ion-icon class="ion-color" name="check_circle" [fill]="true"></ion-icon> </ion-chip>
}
<ion-label>{{ displayValue }}</ion-label>
</ion-chip>
}

View File

@@ -32,7 +32,6 @@ import {ScheduleProvider} from '../calendar/schedule.provider';
import {GeoNavigationDirective} from '../map/geo-navigation.directive'; import {GeoNavigationDirective} from '../map/geo-navigation.directive';
import {MapWidgetComponent} from '../map/widget/map-widget.component'; import {MapWidgetComponent} from '../map/widget/map-widget.component';
import {MenuModule} from '../menu/menu.module'; import {MenuModule} from '../menu/menu.module';
import {SettingsProvider} from '../settings/settings.provider';
import {StorageModule} from '../storage/storage.module'; import {StorageModule} from '../storage/storage.module';
import {ActionChipListComponent} from './chips/action-chip-list.component'; import {ActionChipListComponent} from './chips/action-chip-list.component';
import {AddEventActionChipComponent} from './chips/data/add-event-action-chip.component'; import {AddEventActionChipComponent} from './chips/data/add-event-action-chip.component';
@@ -214,7 +213,6 @@ import {ShareButtonComponent} from './elements/share-button.component';
StAppsWebHttpClient, StAppsWebHttpClient,
CalendarService, CalendarService,
RoutingStackService, RoutingStackService,
SettingsProvider,
{ {
provide: SimpleBrowser, provide: SimpleBrowser,
useFactory: browserFactory, useFactory: browserFactory,

View File

@@ -1,112 +1,105 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
@if (showModalHeader) { <ion-header *ngIf="showModalHeader" translucent>
<ion-header translucent> <ion-toolbar color="primary" mode="ios">
<ion-toolbar color="primary" mode="ios"> <ion-title>{{ 'name' | thingTranslate : item }}</ion-title>
<ion-title>{{ 'name' | thingTranslate: item }}</ion-title> <ion-buttons slot="end">
<ion-buttons slot="end"> <ion-button (click)="modalController.dismiss()">{{ 'app.ui.CLOSE' | translate }}</ion-button>
<ion-button (click)="modalController.dismiss()">{{ 'app.ui.CLOSE' | translate }}</ion-button> </ion-buttons>
</ion-buttons> </ion-toolbar>
</ion-toolbar> </ion-header>
</ion-header>
} <stapps-title-card *ngIf="!showModalHeader" [item]="item"> </stapps-title-card>
@if (!showModalHeader) {
<stapps-title-card [item]="item"> </stapps-title-card>
}
<ng-container *ngTemplateOutlet="contentTemplateRef || defaultContent; context: {$implicit: item}"> <ng-container *ngTemplateOutlet="contentTemplateRef || defaultContent; context: {$implicit: item}">
</ng-container> </ng-container>
<stapps-origin-detail [origin]="item.origin"></stapps-origin-detail> <stapps-origin-detail [origin]="item.origin"></stapps-origin-detail>
<ng-template #defaultContent> <ng-template #defaultContent>
<div class="content-switch"> <div [ngSwitch]="item.type" class="content-switch">
@switch (item.type) { <stapps-article-detail-content
@case ('article') { [item]="$any(item)"
<stapps-article-detail-content [item]="$any(item)"></stapps-article-detail-content> *ngSwitchCase="'article'"
} ></stapps-article-detail-content>
@case ('catalog') { <stapps-catalog-detail-content
<stapps-catalog-detail-content [item]="$any(item)"></stapps-catalog-detail-content> [item]="$any(item)"
} *ngSwitchCase="'catalog'"
@case ('date series') { ></stapps-catalog-detail-content>
<stapps-date-series-detail-content [item]="$any(item)"></stapps-date-series-detail-content> <stapps-date-series-detail-content
} [item]="$any(item)"
@case ('dish') { *ngSwitchCase="'date series'"
<stapps-dish-detail-content [item]="$any(item)"></stapps-dish-detail-content> ></stapps-date-series-detail-content>
} <stapps-dish-detail-content [item]="$any(item)" *ngSwitchCase="'dish'"></stapps-dish-detail-content>
@case ('academic event') { <stapps-event-detail-content
<stapps-event-detail-content [item]="$any(item)"></stapps-event-detail-content> [item]="$any(item)"
} *ngSwitchCase="'academic event'"
@case ('sport course') { ></stapps-event-detail-content>
<stapps-event-detail-content [item]="$any(item)"></stapps-event-detail-content> <stapps-event-detail-content
} [item]="$any(item)"
@case ('favorite') { *ngSwitchCase="'sport course'"
<stapps-favorite-detail-content [item]="$any(item)"></stapps-favorite-detail-content> ></stapps-event-detail-content>
} <stapps-favorite-detail-content
@case ('message') { [item]="$any(item)"
<stapps-message-detail-content [item]="$any(item)"></stapps-message-detail-content> *ngSwitchCase="'favorite'"
} ></stapps-favorite-detail-content>
@case ('job posting') { <stapps-message-detail-content
<stapps-job-posting-detail-content [item]="$any(item)"></stapps-job-posting-detail-content> [item]="$any(item)"
} *ngSwitchCase="'message'"
@case ('person') { ></stapps-message-detail-content>
<stapps-person-detail-content [item]="$any(item)"></stapps-person-detail-content> <stapps-job-posting-detail-content
} [item]="$any(item)"
@case ('building') { *ngSwitchCase="'job posting'"
<stapps-place-detail-content [item]="$any(item)"></stapps-place-detail-content> ></stapps-job-posting-detail-content>
} <stapps-person-detail-content [item]="$any(item)" *ngSwitchCase="'person'"></stapps-person-detail-content>
@case ('floor') { <stapps-place-detail-content [item]="$any(item)" *ngSwitchCase="'building'"></stapps-place-detail-content>
<stapps-place-detail-content [item]="$any(item)"></stapps-place-detail-content> <stapps-place-detail-content [item]="$any(item)" *ngSwitchCase="'floor'"></stapps-place-detail-content>
} <stapps-place-detail-content
@case ('point of interest') { [item]="$any(item)"
<stapps-place-detail-content [item]="$any(item)"></stapps-place-detail-content> *ngSwitchCase="'point of interest'"
} ></stapps-place-detail-content>
@case ('room') { <stapps-place-detail-content
<stapps-place-detail-content [item]="$any(item)"
[item]="$any(item)" [openAsModal]="openAsModal"
[openAsModal]="openAsModal" *ngSwitchCase="'room'"
></stapps-place-detail-content> ></stapps-place-detail-content>
} <stapps-semester-detail-content
@case ('semester') { [item]="$any(item)"
<stapps-semester-detail-content [item]="$any(item)"></stapps-semester-detail-content> *ngSwitchCase="'semester'"
} ></stapps-semester-detail-content>
@case ('video') { <stapps-video-detail-content [item]="$any(item)" *ngSwitchCase="'video'"></stapps-video-detail-content>
<stapps-video-detail-content [item]="$any(item)"></stapps-video-detail-content> <ng-container *ngSwitchDefault>
} <ion-item class="ion-text-wrap" lines="inset">
@default { <ion-thumbnail slot="start" class="ion-margin-end">
<ion-item class="ion-text-wrap" lines="inset"> <ion-icon [name]="item.type | dataIcon"></ion-icon>
<ion-thumbnail slot="start" class="ion-margin-end"> </ion-thumbnail>
<ion-icon [name]="item.type | dataIcon"></ion-icon> <ion-grid>
</ion-thumbnail> <ion-row>
<ion-grid> <ion-col>
<ion-row> <div class="ion-text-wrap">
<ion-col> <h2 class="name">{{ item.name }}</h2>
<div class="ion-text-wrap"> <ion-note>{{ item.type }}</ion-note>
<h2 class="name">{{ item.name }}</h2> </div>
<ion-note>{{ item.type }}</ion-note> </ion-col>
</div> </ion-row>
</ion-col> </ion-grid>
</ion-row> </ion-item>
</ion-grid> <stapps-simple-card
</ion-item> *ngIf="item.description"
@if (item.description) { [title]="$any('description' | propertyNameTranslate : item) | titlecase"
<stapps-simple-card [content]="'description' | thingTranslate : item"
[title]="$any('description' | propertyNameTranslate: item) | titlecase" ></stapps-simple-card>
[content]="'description' | thingTranslate: item" </ng-container>
></stapps-simple-card>
}
}
}
</div> </div>
</ng-template> </ng-template>

View File

@@ -41,7 +41,8 @@ stapps-origin-detail {
width: 100%; width: 100%;
height: fit-content; height: fit-content;
margin-block: calc((var(--header-spacing-bottom) - var(--spacing-xl)) * -1) var(--spacing-xl); margin-block-start: calc((var(--header-spacing-bottom) - var(--spacing-xl)) * -1);
margin-block-end: var(--spacing-xl);
background-color: var(--ion-card-background); background-color: var(--ion-card-background);

View File

@@ -15,13 +15,13 @@
import {Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef} from '@angular/core'; import {Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import {ModalController} from '@ionic/angular'; import {ModalController} from '@ionic/angular';
import {LangChangeEvent, TranslateService} from '@ngx-translate/core'; import {SCSaveableThing, SCThings, SCUuid} from '@openstapps/core';
import {SCLanguageCode, SCSaveableThing, SCThings, SCUuid} from '@openstapps/core';
import {DataProvider, DataScope} from '../data.provider'; import {DataProvider, DataScope} from '../data.provider';
import {FavoritesService} from '../../favorites/favorites.service'; import {FavoritesService} from '../../favorites/favorites.service';
import {take} from 'rxjs/operators'; import {take} from 'rxjs/operators';
import {Network} from '@capacitor/network'; import {Network} from '@capacitor/network';
import {DataListContext} from '../list/data-list.component'; import {DataListContext} from '../list/data-list.component';
import {lastValueFrom} from 'rxjs';
export interface ExternalDataLoadEvent { export interface ExternalDataLoadEvent {
uid: SCUuid; uid: SCUuid;
@@ -29,6 +29,13 @@ export interface ExternalDataLoadEvent {
resolve: (item: SCThings | null | undefined) => void; resolve: (item: SCThings | null | undefined) => void;
} }
/**
* Type guard for SCSavableThing
*/
function isSCSavableThing(thing: SCThings | SCSaveableThing): thing is SCSaveableThing {
return (thing as SCSaveableThing).data !== undefined;
}
/** /**
* A Component to display an SCThing detailed * A Component to display an SCThing detailed
*/ */
@@ -53,11 +60,6 @@ export class DataDetailComponent implements OnInit {
@Input() autoRouteDataPath = true; @Input() autoRouteDataPath = true;
/**
* The language of the item
*/
language: SCLanguageCode;
/** /**
* Indicating wether internet connectivity is given or not * Indicating wether internet connectivity is given or not
*/ */
@@ -79,20 +81,12 @@ export class DataDetailComponent implements OnInit {
@Output() loadItem: EventEmitter<ExternalDataLoadEvent> = new EventEmitter<ExternalDataLoadEvent>(); @Output() loadItem: EventEmitter<ExternalDataLoadEvent> = new EventEmitter<ExternalDataLoadEvent>();
/**
* Type guard for SCSavableThing
*/
static isSCSavableThing(thing: SCThings | SCSaveableThing): thing is SCSaveableThing {
return (thing as SCSaveableThing).data !== undefined;
}
constructor( constructor(
protected readonly route: ActivatedRoute, protected readonly route: ActivatedRoute,
router: Router, router: Router,
private readonly dataProvider: DataProvider, private readonly dataProvider: DataProvider,
private readonly favoritesService: FavoritesService, private readonly favoritesService: FavoritesService,
readonly modalController: ModalController, readonly modalController: ModalController,
translateService: TranslateService,
) { ) {
this.inputItem = router.getCurrentNavigation()?.extras.state?.item; this.inputItem = router.getCurrentNavigation()?.extras.state?.item;
if (!this.inputItem?.origin) { if (!this.inputItem?.origin) {
@@ -100,10 +94,6 @@ export class DataDetailComponent implements OnInit {
// This can happen, for example, when detail views use `inPlace` list items // This can happen, for example, when detail views use `inPlace` list items
delete this.inputItem; delete this.inputItem;
} }
this.language = translateService.currentLang as SCLanguageCode;
translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.language = event.lang as SCLanguageCode;
});
this.isDisconnected = new Promise(async resolve => { this.isDisconnected = new Promise(async resolve => {
const isConnected = (await Network.getStatus()).connected; const isConnected = (await Network.getStatus()).connected;
@@ -126,13 +116,8 @@ export class DataDetailComponent implements OnInit {
) )
: this.dataProvider.get(uid, DataScope.Remote))); : this.dataProvider.get(uid, DataScope.Remote)));
this.item = item // eslint-disable-next-line unicorn/no-null
? // eslint-disable-next-line unicorn/no-null this.item = item ? (isSCSavableThing(item) ? item.data : item) : null;
DataDetailComponent.isSCSavableThing(item)
? item.data
: item
: // eslint-disable-next-line unicorn/no-null
null;
} catch { } catch {
// eslint-disable-next-line unicorn/no-null // eslint-disable-next-line unicorn/no-null
this.item = null; this.item = null;
@@ -144,14 +129,10 @@ export class DataDetailComponent implements OnInit {
await this.getItem(uid ?? '', false); await this.getItem(uid ?? '', false);
// fallback to the saved item (from favorites) // fallback to the saved item (from favorites)
if (this.item === null) { if (this.item === null) {
this.favoritesService const item = await lastValueFrom(this.favoritesService.get(uid).pipe(take(1)));
.get(uid) if (item) {
.pipe(take(1)) this.item = item.data;
.subscribe(item => { }
if (item !== undefined) {
this.item = item.data;
}
});
} }
} }
} }

View File

@@ -1,51 +1,40 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
@if (defaultHeader) { <ion-header *ngIf="defaultHeader">
<ion-header> <ion-toolbar color="primary" mode="ios">
<ion-toolbar color="primary" mode="ios"> <ion-buttons slot="start" *ngIf="!isModal">
@if (!isModal) { <ion-back-button></ion-back-button>
<ion-buttons slot="start"> </ion-buttons>
<ion-back-button></ion-back-button> <ion-title
</ion-buttons> *ngIf="item"
} [style.opacity]="(collapse ? '1' : '0') + '!important'"
@if (item) { [style.translate]="collapse ? '0' : '0 10px'"
<ion-title >{{ 'name' | thingTranslate: item }}</ion-title
[style.opacity]="(collapse ? '1' : '0') + '!important'" >
[style.translate]="collapse ? '0' : '0 10px'" <ion-buttons [slot]="isModal ? 'start' : 'primary'">
>{{ 'name' | thingTranslate: item }}</ion-title <stapps-share-button *ngIf="item" [title]="'name' | thingTranslate: item"></stapps-share-button>
> <stapps-favorite-button *ngIf="item" [item]="$any(item)"></stapps-favorite-button>
} </ion-buttons>
<ion-buttons [slot]="isModal ? 'start' : 'primary'"> <ion-buttons slot="end" *ngIf="isModal">
@if (item) { <ion-button fill="clear" (click)="modalController.dismiss()">
<stapps-share-button [title]="'name' | thingTranslate: item"></stapps-share-button> <ion-label>{{ 'modal.DISMISS' | translate }}</ion-label>
} </ion-button>
@if (item) { </ion-buttons>
<stapps-favorite-button [item]="$any(item)"></stapps-favorite-button> </ion-toolbar>
} </ion-header>
</ion-buttons>
@if (isModal) {
<ion-buttons slot="end">
<ion-button fill="clear" (click)="modalController.dismiss()">
<ion-label>{{ 'modal.DISMISS' | translate }}</ion-label>
</ion-button>
</ion-buttons>
}
</ion-toolbar>
</ion-header>
}
<ng-content select="[header]"></ng-content> <ng-content select="[header]"></ng-content>
<ion-content <ion-content
parallax parallax
@@ -53,31 +42,31 @@
[scrollEvents]="true" [scrollEvents]="true"
(ionScroll)="collapse = $any($event).detail.scrollTop > 50" (ionScroll)="collapse = $any($event).detail.scrollTop > 50"
> >
@switch (true) { <ng-container [ngSwitch]="true">
@case (!item && (isDisconnected | async)) { <ng-container *ngSwitchCase="!item && (isDisconnected | async)">
<div class="centered-message-container"> <div class="centered-message-container">
<ion-icon name="signal_disconnected"></ion-icon> <ion-icon name="signal_disconnected"></ion-icon>
<ion-label> {{ 'data.detail.COULD_NOT_CONNECT' | translate }} </ion-label> <ion-label> {{ 'data.detail.COULD_NOT_CONNECT' | translate }} </ion-label>
</div> </div>
} </ng-container>
@case (item === null) { <ng-container *ngSwitchCase="item === null">
<div class="centered-message-container"> <div class="centered-message-container">
<ion-icon name="link_off"></ion-icon> <ion-icon name="link_off"></ion-icon>
<ion-label> {{ 'data.detail.NOT_FOUND' | translate }} </ion-label> <ion-label> {{ 'data.detail.NOT_FOUND' | translate }} </ion-label>
</div> </div>
} </ng-container>
@case (!item && item !== null) { <ng-container *ngSwitchCase="!item && item !== null">
<stapps-skeleton-list-item></stapps-skeleton-list-item> <stapps-skeleton-list-item></stapps-skeleton-list-item>
<stapps-skeleton-simple-card></stapps-skeleton-simple-card> <stapps-skeleton-simple-card></stapps-skeleton-simple-card>
} </ng-container>
@default { <ng-container *ngSwitchDefault>
@if (item) { <ng-container *ngIf="item">
<stapps-data-path [item]="item" [autoRouting]="autoRouteDataPath"></stapps-data-path> <stapps-data-path [item]="item" [autoRouting]="autoRouteDataPath"></stapps-data-path>
<stapps-data-detail-content <stapps-data-detail-content
[item]="item" [item]="item"
[contentTemplateRef]="contentTemplateRef" [contentTemplateRef]="contentTemplateRef"
></stapps-data-detail-content> ></stapps-data-detail-content>
} </ng-container>
} </ng-container>
} </ng-container>
</ion-content> </ion-content>

View File

@@ -1,19 +1,19 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
@if (path | async; as stack) { <ng-container *ngIf="path | async as stack">
<ion-breadcrumbs <ion-breadcrumbs
color="light" color="light"
[itemsBeforeCollapse]="1" [itemsBeforeCollapse]="1"
@@ -21,23 +21,21 @@
[maxItems]="maxItems" [maxItems]="maxItems"
(ionCollapsedClick)="maxItems = undefined" (ionCollapsedClick)="maxItems = undefined"
> >
@for (fragment of stack; track fragment) { <ion-breadcrumb *ngFor="let fragment of stack">
<ion-breadcrumb> <ion-label
<ion-label (click)="dataRoutingService.emitPathEvent(fragment)"
(click)="dataRoutingService.emitPathEvent(fragment)" [style.max-width]="
[style.max-width]=" stack.length === 1
stack.length === 1 ? '100%'
? '100%' : stack.length === 2
: stack.length === 2 ? '40vw'
? '40vw' : (($width | async) ?? 0) >= 768
: (($width | async) ?? 0) >= 768 ? '30vw'
? '30vw' : 'calc(100vw - 120px)'
: 'calc(100vw - 120px)' "
" class="crumb-label"
class="crumb-label" >{{ 'name' | thingTranslate : $any(fragment) }}</ion-label
>{{ 'name' | thingTranslate: $any(fragment) }}</ion-label >
> </ion-breadcrumb>
</ion-breadcrumb>
}
</ion-breadcrumbs> </ion-breadcrumbs>
} </ng-container>

View File

@@ -1,17 +1,17 @@
<!-- <!--
~ Copyright (C) 2022 StApps ~ Copyright (C) 2022 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
<ion-card> <ion-card>
<ion-card-header>{{ 'data.detail.address.TITLE' | translate | titlecase }}</ion-card-header> <ion-card-header>{{ 'data.detail.address.TITLE' | translate | titlecase }}</ion-card-header>
@@ -29,22 +29,18 @@
<ion-col>{{ 'data.detail.address.CITY' | translate | titlecase }}:</ion-col> <ion-col>{{ 'data.detail.address.CITY' | translate | titlecase }}:</ion-col>
<ion-col width-60 text-right> {{ address.addressLocality }} </ion-col> <ion-col width-60 text-right> {{ address.addressLocality }} </ion-col>
</ion-row> </ion-row>
@if (address.addressRegion) { <ion-row *ngIf="address.addressRegion">
<ion-row> <ion-col>{{ 'data.detail.address.REGION' | translate | titlecase }}:</ion-col>
<ion-col>{{ 'data.detail.address.REGION' | translate | titlecase }}:</ion-col> <ion-col width-60 text-right> {{ address.addressRegion }} </ion-col>
<ion-col width-60 text-right> {{ address.addressRegion }} </ion-col> </ion-row>
</ion-row>
}
<ion-row> <ion-row>
<ion-col>{{ 'data.detail.address.COUNTRY' | translate | titlecase }}:</ion-col> <ion-col>{{ 'data.detail.address.COUNTRY' | translate | titlecase }}:</ion-col>
<ion-col width-60 text-right> {{ address.addressCountry }} </ion-col> <ion-col width-60 text-right> {{ address.addressCountry }} </ion-col>
</ion-row> </ion-row>
@if (address.postOfficeBoxNumber) { <ion-row *ngIf="address.postOfficeBoxNumber">
<ion-row> <ion-col>{{ 'data.detail.address.POST_OFFICE_BOX' | translate | titlecase }}</ion-col>
<ion-col>{{ 'data.detail.address.POST_OFFICE_BOX' | translate | titlecase }}</ion-col> <ion-col width-60 text-right> {{ address.postOfficeBoxNumber }} </ion-col>
<ion-col width-60 text-right> {{ address.postOfficeBoxNumber }} </ion-col> </ion-row>
</ion-row>
}
</ion-grid> </ion-grid>
</ion-card-content> </ion-card-content>
</ion-card> </ion-card>

View File

@@ -1,22 +1,22 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
<ion-card> <ion-card>
<ion-card-header>{{ 'data.types.certification.TITLE' | translate }}</ion-card-header> <ion-card-header>{{ 'data.types.certification.TITLE' | translate }}</ion-card-header>
<ion-card-content> <ion-card-content>
<div class="certification-list"> <div class="certification-list">
@for (cert of certifications; track cert) { <ng-container *ngFor="let cert of certifications">
<img <img
(click)="popover.present($event)" (click)="popover.present($event)"
[width]="72" [width]="72"
@@ -31,7 +31,7 @@
</ion-content> </ion-content>
</ng-template> </ng-template>
</ion-popover> </ion-popover>
} </ng-container>
</div> </div>
<ion-note> <ion-note>
<stapps-external-link <stapps-external-link

View File

@@ -1,34 +1,28 @@
<!-- <!--
~ Copyright (C) 2022 StApps ~ Copyright (C) 2022 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
@if (text !== undefined) { <ng-container *ngIf="text !== undefined">
<span class="ion-hide-sm-up"> <span class="ion-hide-sm-up">
{{ text | slice: 0 : size }} {{ text | slice : 0 : size }}
@if (text.length > size) { <span *ngIf="text.length > size" class="ion-hide-sm-up"></span>
<span class="ion-hide-sm-up"></span>
}
</span> </span>
<span class="ion-hide-sm-down ion-hide-md-up"> <span class="ion-hide-sm-down ion-hide-md-up">
{{ text | slice: 0 : size * 2 }} {{ text | slice : 0 : size * 2 }}
@if (text.length > size * 2) { <span *ngIf="text.length > size * 2" class="ion-hide-sm-down ion-hide-md-up"></span>
<span class="ion-hide-sm-down ion-hide-md-up"></span>
}
</span> </span>
<span class="ion-hide-md-down"> <span class="ion-hide-md-down">
{{ text | slice: 0 : size * 3 }} {{ text | slice : 0 : size * 3 }}
@if (text.length > size * 3) { <span *ngIf="text.length > size * 3" class="ion-hide-md-down"></span>
<span class="ion-hide-md-down"></span>
}
</span> </span>
} </ng-container>

View File

@@ -1,74 +1,60 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
<ion-card> <ion-card>
<ion-card-header>{{ 'data.detail.offers.TITLE' | translate | titlecase }}</ion-card-header> <ion-card-header>{{ 'data.detail.offers.TITLE' | translate | titlecase }}</ion-card-header>
<ion-card-content> <ion-card-content>
@for (offer of offers; track offer) { <div *ngFor="let offer of offers">
<div> <ion-grid>
<ion-grid> <ion-row>
<ion-row> <ion-col *ngIf="offer.inPlace">
@if (offer.inPlace) { <ion-icon name="pin_drop"></ion-icon>
<ion-col> <a [routerLink]="['/data-detail', offer.inPlace.uid]">
<ion-icon name="pin_drop"></ion-icon> {{ 'name' | thingTranslate : offer.inPlace }}
<a [routerLink]="['/data-detail', offer.inPlace.uid]"> </a>
{{ 'name' | thingTranslate: offer.inPlace }} </ion-col>
</a> <ion-col *ngIf="offer.availabilityRange">
</ion-col> <span
} *ngIf="offer.availabilityRange.gt ? offer.availabilityRange.gt : offer.availabilityRange.gte"
@if (offer.availabilityRange) { >
<ion-col> {{ (offer.availabilityRange.gt ? offer.availabilityRange.gt : offer.availabilityRange.gte) |
@if (offer.availabilityRange.gt ? offer.availabilityRange.gt : offer.availabilityRange.gte) { amDateFormat : 'll' }}
<span> </span>
{{ </ion-col>
(offer.availabilityRange.gt ? offer.availabilityRange.gt : offer.availabilityRange.gte) </ion-row>
| amDateFormat: 'll' </ion-grid>
}} <ion-grid *ngIf="offer.prices && offer.availability !== 'out of stock'">
</span> <ion-row *ngFor="let group of $any(offer.prices) | keyvalue">
} <ng-container *ngIf="group.key !== 'default'">
</ion-col> <ion-col>{{ 'data.detail.offers.' + group.key | translate }} </ion-col>
} <ion-col width-20 text-right>
</ion-row> <p>{{ $any(group.value) | currency : 'EUR' : 'symbol' : undefined : 'de' }}</p>
</ion-grid> </ion-col>
@if (offer.prices && offer.availability !== 'out of stock') { </ng-container>
<ion-grid> </ion-row>
@for (group of $any(offer.prices) | keyvalue; track group) { </ion-grid>
<ion-row> <ion-grid *ngIf="offer.availability === 'out of stock'">
@if (group.key !== 'default') { <ion-row>
<ion-col>{{ 'data.detail.offers.' + group.key | translate }} </ion-col> <ion-col></ion-col>
<ion-col width-20 text-right> <ion-col width-20 text-right>
<p>{{ $any(group.value) | currency: 'EUR' : 'symbol' : undefined : 'de' }}</p> <ion-text color="danger">
</ion-col> <p>{{ 'data.detail.offers.sold_out' | translate }}</p>
} </ion-text>
</ion-row> </ion-col>
} </ion-row>
</ion-grid> </ion-grid>
} </div>
@if (offer.availability === 'out of stock') {
<ion-grid>
<ion-row>
<ion-col></ion-col>
<ion-col width-20 text-right>
<ion-text color="danger">
<p>{{ 'data.detail.offers.sold_out' | translate }}</p>
</ion-text>
</ion-col>
</ion-row>
</ion-grid>
}
</div>
}
</ion-card-content> </ion-card-content>
</ion-card> </ion-card>

View File

@@ -13,34 +13,34 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Component, Input} from '@angular/core'; import {Component, Input} from '@angular/core';
import { import {SCAcademicPriceGroup, SCThingThatCanBeOfferedOffer} from '@openstapps/core';
SCAcademicPriceGroup,
SCThingThatCanBeOfferedAvailability,
SCThingThatCanBeOfferedOffer,
} from '@openstapps/core';
import {SettingsProvider} from '../../settings/settings.provider'; import {SettingsProvider} from '../../settings/settings.provider';
/**
* TODO
*/
@Component({ @Component({
selector: 'stapps-offers-in-list', selector: 'stapps-offers-in-list',
templateUrl: 'offers-in-list.html', templateUrl: 'offers-in-list.html',
styleUrls: ['offers-in-list.scss'],
}) })
export class OffersInListComponent { export class OffersInListComponent {
/**
* TODO
*/
@Input() set offers(it: Array<SCThingThatCanBeOfferedOffer<SCAcademicPriceGroup>>) { @Input() set offers(it: Array<SCThingThatCanBeOfferedOffer<SCAcademicPriceGroup>>) {
this._offers = it; this._offers = it;
this.price = it[0].prices?.default; this.price = it[0].prices?.default;
this.settingsProvider.getSetting('profile', 'group').then(group => { this.settingsProvider.getSetting<string>('profile', 'group').then(group => {
this.price = it[0].prices?.[(group.value as string).replace(/s$/, '') as never]; this.price = it[0].prices?.[group.replace(/s$/, '') as never];
}); });
if (it.length === 1) { const availabilities = new Set(it.map(offer => offer.availability));
this.availability = it[0].availability; this.soldOut = availabilities.has('out of stock') && availabilities.size === 1;
}
} }
price?: number; price?: number;
availability: SCThingThatCanBeOfferedAvailability; soldOut: boolean;
_offers: Array<SCThingThatCanBeOfferedOffer<SCAcademicPriceGroup>>; _offers: Array<SCThingThatCanBeOfferedOffer<SCAcademicPriceGroup>>;

View File

@@ -1,32 +1,28 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
@if (price) { <div>
<ion-badge <ion-text *ngIf="price && !soldOut" style="white-space: nowrap">
[color]=" <h2>{{ price | currency : 'EUR' : 'symbol' : undefined : 'de' }}</h2>
availability === 'out of stock' </ion-text>
? 'danger' <ion-text *ngIf="soldOut" color="danger" class="sold-out" style="white-space: nowrap">
: availability === 'limited availability' <h2>{{ 'data.detail.offers.sold_out' | translate }}</h2>
? 'warning' </ion-text>
: 'primary' <p *ngIf="_offers[0].inPlace && !soldOut" class="place" style="white-space: nowrap">
" <ion-icon name="pin_drop"></ion-icon>{{ _offers[0].inPlace.name }}<span *ngIf="_offers.length > 1"
> >...</span
{{ >
availability === 'out of stock' </p>
? ('data.detail.offers.sold_out' | translate) </div>
: (price | currency: 'EUR' : 'symbol' : undefined : 'de')
}}
</ion-badge>
}

View File

@@ -1,14 +0,0 @@
:host {
display: contents;
}
ion-badge {
translate: 0 0.25em;
display: inline-block;
margin-top: -0.25em;
padding: 0.25em;
font-size: 0.8em;
}

View File

@@ -1,84 +1,65 @@
<!-- <!--
~ Copyright (C) 2022 StApps ~ Copyright (C) 2022 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
@if (origin.type === 'user') { <ion-card *ngIf="origin.type === 'user'">
<ion-card> <ion-card-header
<ion-card-header >{{ 'data.types.origin.TITLE' | translate | titlecase }}: {{ 'data.types.origin.USER' | translate |
>{{ 'data.types.origin.TITLE' | translate | titlecase }}: titlecase }}</ion-card-header
{{ 'data.types.origin.USER' | translate | titlecase }}</ion-card-header >
> <ion-card-content>
<ion-card-content> <p>
<p> {{ 'data.types.origin.detail.CREATED' | translate | titlecase }}: {{ origin.created | amDateFormat :
{{ 'data.types.origin.detail.CREATED' | translate | titlecase }}: 'll' }}
{{ origin.created | amDateFormat: 'll' }} </p>
</p> <p *ngIf="origin.updated">
@if (origin.updated) { {{ 'data.types.origin.detail.UPDATED' | translate | titlecase }}: {{ origin.updated | amDateFormat :
<p> 'll' }}
{{ 'data.types.origin.detail.UPDATED' | translate | titlecase }}: </p>
{{ origin.updated | amDateFormat: 'll' }} <p *ngIf="origin.modified">
</p> {{ 'data.types.origin.detail.MODIFIED' | translate | titlecase }}: {{ origin.modified | amDateFormat :
} 'll' }}
@if (origin.modified) { </p>
<p> <p *ngIf="origin.maintainer">
{{ 'data.types.origin.detail.MODIFIED' | translate | titlecase }}: {{ 'data.types.origin.detail.MAINTAINER' | translate }}:
{{ origin.modified | amDateFormat: 'll' }} <a [routerLink]="['/data-detail', origin.maintainer.uid]">{{ origin.maintainer.name }}</a>
</p> </p>
} </ion-card-content>
@if (origin.maintainer) { </ion-card>
<p>
{{ 'data.types.origin.detail.MAINTAINER' | translate }}: <ion-card *ngIf="origin.type === 'remote'">
<a [routerLink]="['/data-detail', origin.maintainer.uid]">{{ origin.maintainer.name }}</a> <ion-card-header
</p> >{{ 'data.types.origin.TITLE' | translate | titlecase }}: {{ 'data.types.origin.REMOTE' | translate |
} titlecase }}</ion-card-header
</ion-card-content> >
</ion-card> <ion-card-content>
} <p>
@if (origin.type === 'remote') { {{ 'data.types.origin.detail.INDEXED' | translate | titlecase }}: {{ origin.indexed | amDateFormat :
<ion-card> 'll' }}
<ion-card-header </p>
>{{ 'data.types.origin.TITLE' | translate | titlecase }}: <p *ngIf="origin.modified">
{{ 'data.types.origin.REMOTE' | translate | titlecase }}</ion-card-header {{ 'data.types.origin.detail.MODIFIED' | translate | titlecase }}: {{ origin.modified | amDateFormat :
> 'll' }}
<ion-card-content> </p>
<p> <p *ngIf="origin.name">{{ 'data.types.origin.detail.MAINTAINER' | translate }}: {{ origin.name }}</p>
{{ 'data.types.origin.detail.INDEXED' | translate | titlecase }}: <p *ngIf="origin.maintainer">
{{ origin.indexed | amDateFormat: 'll' }} {{ 'data.types.origin.detail.MAINTAINER' | translate | titlecase }}:
</p> <a [routerLink]="['/data-detail', origin.maintainer.uid]">{{ origin.maintainer.name }}</a>
@if (origin.modified) { </p>
<p> <p *ngIf="origin.responsibleEntity">
{{ 'data.types.origin.detail.MODIFIED' | translate | titlecase }}: {{ 'data.types.origin.detail.RESPONSIBLE' | translate | titlecase }}:
{{ origin.modified | amDateFormat: 'll' }} <a [routerLink]="['/data-detail', origin.responsibleEntity.uid]">{{ origin.responsibleEntity.name }}</a>
</p> </p>
} </ion-card-content>
@if (origin.name) { </ion-card>
<p>{{ 'data.types.origin.detail.MAINTAINER' | translate }}: {{ origin.name }}</p>
}
@if (origin.maintainer) {
<p>
{{ 'data.types.origin.detail.MAINTAINER' | translate | titlecase }}:
<a [routerLink]="['/data-detail', origin.maintainer.uid]">{{ origin.maintainer.name }}</a>
</p>
}
@if (origin.responsibleEntity) {
<p>
{{ 'data.types.origin.detail.RESPONSIBLE' | translate | titlecase }}:
<a [routerLink]="['/data-detail', origin.responsibleEntity.uid]">{{
origin.responsibleEntity.name
}}</a>
</p>
}
</ion-card-content>
</ion-card>
}

View File

@@ -1,10 +1,7 @@
@if (origin.type === 'user') { <div *ngIf="origin.type === 'user'">
<div> <p>{{ origin.created | dateFormat }}</p>
<p>{{ origin.created | dateFormat }}</p> </div>
</div>
} <div *ngIf="origin.type === 'remote'">
@if (origin.type === 'remote') { <p>{{ origin.indexed | dateFormat }}</p>
<div> </div>
<p>{{ origin.indexed | dateFormat }}</p>
</div>
}

View File

@@ -1,39 +1,40 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
@if (canBeRated | async) { <ion-button
<ion-button *ngIf="canBeRated | async"
fill="clear" fill="clear"
(click)="$event.stopPropagation(); performRating.next(true)" (click)="$event.stopPropagation(); performRating.next(true)"
[disabled]="wasAlreadyRated | async" [disabled]="wasAlreadyRated | async"
> >
<ion-icon slot="icon-only" color="medium" name="thumbs_up_down"></ion-icon> <ion-icon slot="icon-only" color="medium" name="thumbs_up_down"></ion-icon>
</ion-button> </ion-button>
}
@if ((performRating | async) && (wasAlreadyRated | async) !== true) { <div
<div class="rating-stars" [@rating]="(userRating | async) === undefined ? 'abandoned' : 'rated'"> class="rating-stars"
@for (i of [5, 4, 3, 2, 1]; track i) { *ngIf="(performRating | async) && (wasAlreadyRated | async) !== true"
<ion-icon [@rating]="(userRating | async) === undefined ? 'abandoned' : 'rated'"
[class.rated-value]="(userRating | async) === i" >
(click)="$event.stopPropagation(); userRating.next(i)" <ion-icon
slot="icon-only" [class.rated-value]="(userRating | async) === i"
[size]="32" *ngFor="let i of [5, 4, 3, 2, 1]"
color="medium" (click)="$event.stopPropagation(); userRating.next(i)"
name="grade" slot="icon-only"
></ion-icon> [size]="32"
} color="medium"
<label class="thank-you">{{ 'ratings.thank_you' | translate }}</label> name="grade"
</div> ></ion-icon>
} <label class="thank-you">{{ 'ratings.thank_you' | translate }}</label>
</div>

View File

@@ -1,39 +1,38 @@
<!-- <!--
~ Copyright (C) 2022 StApps ~ Copyright (C) 2022 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
<ion-card> <ion-card>
<ion-card-header>{{ title }}</ion-card-header> <ion-card-header>{{ title }}</ion-card-header>
<ion-card-content> <ion-card-content>
@if (isString(content)) { <ng-container *ngIf="isString(content); else list">
@if (isMarkdown) { <ng-container *ngIf="isMarkdown; else plainText">
<markdown [data]="content"></markdown> <markdown [data]="content"></markdown>
} @else { </ng-container>
<ng-template #plainText>
<p>{{ content }}</p> <p>{{ content }}</p>
} </ng-template>
} @else { </ng-container>
@if (content && isThing(content[0])) { <ng-template #list>
@for (thing of $any(content); track thing) { <ng-container *ngIf="content && isThing(content[0]); else textList">
<a [routerLink]="['/data-detail', thing.uid]"> <a [routerLink]="['/data-detail', thing.uid]" *ngFor="let thing of $any(content)">
<p>{{ 'name' | thingTranslate: thing }}</p> <p>{{ 'name' | thingTranslate : thing }}</p>
</a> </a>
} </ng-container>
} @else { <ng-template #textList>
@for (text of $any(content); track text) { <p *ngFor="let text of $any(content)">{{ text }}</p>
<p>{{ text }}</p> </ng-template>
} </ng-template>
}
}
</ion-card-content> </ion-card-content>
</ion-card> </ion-card>

View File

@@ -1,9 +1,7 @@
<ion-item> <ion-item>
@if (!hideThumbnail) { <ion-thumbnail *ngIf="!hideThumbnail" slot="start" class="ion-margin-end">
<ion-thumbnail slot="start" class="ion-margin-end"> <ion-skeleton-text animated></ion-skeleton-text>
<ion-skeleton-text animated></ion-skeleton-text> </ion-thumbnail>
</ion-thumbnail>
}
<ion-grid> <ion-grid>
<ion-row> <ion-row>
<ion-col> <ion-col>

View File

@@ -1,14 +1,14 @@
<ion-card> <ion-card>
@if (title) { <ion-card-header *ngIf="title">
<ion-card-header> <ion-skeleton-text animated style="width: 15%"></ion-skeleton-text>
<ion-skeleton-text animated style="width: 15%"></ion-skeleton-text> </ion-card-header>
</ion-card-header>
}
<ion-card-content> <ion-card-content>
<p> <p>
@for (line of [].constructor(lines); track line) { <ion-skeleton-text
<ion-skeleton-text animated style="width: 85%"></ion-skeleton-text> *ngFor="let line of [].constructor(lines)"
} animated
style="width: 85%"
></ion-skeleton-text>
</p> </p>
</ion-card-content> </ion-card-content>
</ion-card> </ion-card>

View File

@@ -1,50 +1,49 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
<ion-card> <ion-card>
<ion-card-header> <ion-card-header>
<ion-card-title> <ion-card-title>
<h1> <h1>
@if ($any(item).honorificPrefix) { <ng-container *ngIf="$any(item).honorificPrefix">{{
{{ 'honorificPrefix' | thingTranslate: item }} 'honorificPrefix' | thingTranslate: item
} }}</ng-container>
{{ 'name' | thingTranslate: item }} {{ 'name' | thingTranslate: item }}
@if ($any(item).honorificSuffix) { <ng-container *ngIf="$any(item).honorificSuffix">{{
{{ 'honorificSuffix' | thingTranslate: item }} 'honorificSuffix' | thingTranslate: item
} }}</ng-container>
</h1> </h1>
</ion-card-title> </ion-card-title>
</ion-card-header> </ion-card-header>
<ion-card-content> <ion-card-content>
@if ($any(item).openingHours; as openingHours) { <div *ngIf="$any(item).openingHours as openingHours" class="opening-hours">
<div class="opening-hours"> <stapps-opening-hours [openingHours]="openingHours"></stapps-opening-hours>
<stapps-opening-hours [openingHours]="openingHours"></stapps-opening-hours> </div>
</div>
}
<!-- TODO obviously this is bad style. Tbd where to put the differentiation. Job Postings always have a description, but it's going to be shown in `stapps-job-posting-detail-content` anyways, no need to repeat here. For this view, I would use other fields of the schema.org JobPosting like the `ThingWithCategory.category` --> <!-- TODO obviously this is bad style. Tbd where to put the differentiation. Job Postings always have a description, but it's going to be shown in `stapps-job-posting-detail-content` anyways, no need to repeat here. For this view, I would use other fields of the schema.org JobPosting like the `ThingWithCategory.category` -->
@if (item.description && item.type !== 'job posting') { <div *ngIf="item.description && item.type !== 'job posting'" class="description">
<div class="description"> <div class="text-accordion" [style.-webkit-line-clamp]="descriptionLinesToDisplay" #accordionTextArea>
<div class="text-accordion" [style.-webkit-line-clamp]="descriptionLinesToDisplay" #accordionTextArea> {{ 'description' | thingTranslate: item }}
{{ 'description' | thingTranslate: item }}
</div>
</div> </div>
} </div>
<!-- TODO see above --> <!-- TODO see above -->
@if (item.description && item.type !== 'job posting' && buttonShown) { <ion-button
<ion-button expand="full" fill="clear" (click)="toggleDescriptionAccordion()"> expand="full"
<ion-icon [name]="buttonState" size="large"></ion-icon> fill="clear"
</ion-button> *ngIf="item.description && item.type !== 'job posting' && buttonShown"
} (click)="toggleDescriptionAccordion()"
>
<ion-icon [name]="buttonState" size="large"></ion-icon>
</ion-button>
</ion-card-content> </ion-card-content>
</ion-card> </ion-card>

View File

@@ -16,8 +16,9 @@
.text-accordion { .text-accordion {
overflow: hidden; overflow: hidden;
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical;
text-overflow: ellipsis; text-overflow: ellipsis;
-webkit-box-orient: vertical;
} }
ion-card { ion-card {

View File

@@ -1,24 +1,22 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
<h2>{{ 'name' | thingTranslate: item }}</h2> <h2>{{ 'name' | thingTranslate : item }}</h2>
@if (item.description) { <p *ngIf="item.description">
<p> <stapps-long-inline-text
<stapps-long-inline-text [text]="'description' | thingTranslate : item"
[text]="'description' | thingTranslate: item" [size]="80"
[size]="80" ></stapps-long-inline-text>
></stapps-long-inline-text> </p>
</p>
}

View File

@@ -1,17 +1,17 @@
<!-- <!--
~ Copyright (C) 2023 StApps ~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it ~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free ~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3. ~ Software Foundation, version 3.
~ ~
~ This program is distributed in the hope that it will be useful, but WITHOUT ~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details. ~ more details.
~ ~
~ 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/>.
--> -->
<ion-item <ion-item
class="ion-text-wrap ion-margin" class="ion-text-wrap ion-margin"
@@ -20,29 +20,33 @@
detail="false" detail="false"
(click)="notifySelect()" (click)="notifySelect()"
> >
@if (!hideThumbnail) { <div class="item-height-placeholder"></div>
<ion-thumbnail slot="start" class="ion-margin-end"> <ion-thumbnail slot="start" *ngIf="!hideThumbnail" class="ion-margin-end">
<ion-icon color="dark" [name]="item.type | dataIcon" [size]="36"></ion-icon> <ion-icon color="dark" [name]="item.type | dataIcon" [size]="36"></ion-icon>
</ion-thumbnail> </ion-thumbnail>
} <ng-container *ngIf="contentTemplateRef; else defaultContent">
@if (contentTemplateRef) { <ion-label class="ion-text-wrap" [ngSwitch]="true">
<ng-container *ngTemplateOutlet="contentTemplateRef; context: {$implicit: item}"></ng-container> <div>
} @else { <ng-container *ngTemplateOutlet="contentTemplateRef; context: {$implicit: item}"></ng-container>
</div>
</ion-label>
</ng-container>
<ng-container *ngIf="listItemEndInteraction" [ngSwitch]="item.type">
<stapps-rating *ngSwitchCase="'dish'" [item]="$any(item)"></stapps-rating>
<stapps-favorite-button *ngSwitchDefault [item]="$any(item)"></stapps-favorite-button>
</ng-container>
</ion-item>
<ng-template #defaultContent>
<ion-label class="ion-text-wrap">
<div> <div>
<ng-template [dataListItemHost]="item"></ng-template> <ng-template [dataListItemHost]="item"></ng-template>
@if (listItemChipInteraction && appearance !== 'square') { <stapps-action-chip-list
<stapps-action-chip-list [item]="item"></stapps-action-chip-list> *ngIf="listItemChipInteraction && appearance !== 'square'"
} slot="end"
[item]="item"
></stapps-action-chip-list>
</div> </div>
} </ion-label>
@if (listItemEndInteraction) { </ng-template>
@switch (item.type) {
@case ('dish') {
<stapps-rating slot="end" [item]="$any(item)"></stapps-rating>
}
@default {
<stapps-favorite-button slot="end" [item]="$any(item)"></stapps-favorite-button>
}
}
}
</ion-item>

View File

@@ -20,72 +20,46 @@
ion-item::part(native) { ion-item::part(native) {
height: 100%; height: 100%;
padding: var(--spacing-sm);
} }
ion-item { ion-item {
--border-color: transparent; --border-color: transparent;
--inner-padding-end: 0; --inner-padding-end: 0;
--padding-start: 0;
@include border-radius-in-parallax(var(--border-radius-default)); @include border-radius-in-parallax(var(--border-radius-default));
overflow: hidden; overflow: hidden;
height: 100%;
margin: var(--spacing-sm); margin: var(--spacing-sm);
ion-thumbnail { ion-thumbnail {
--ion-margin: var(--spacing-xs); --ion-margin: var(--spacing-xs);
margin-block: auto; margin-inline-start: var(--spacing-md);
margin-inline: var(--spacing-md);
padding: 0; padding: 0;
} }
ion-label {
width: 100%;
margin-right: 0;
padding-left: var(--spacing-sm);
div {
display: flex;
flex-direction: column;
}
}
} }
.ion-text-wrap ::ng-deep ion-label { .ion-text-wrap ::ng-deep ion-label {
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical;
white-space: normal !important; white-space: normal !important;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3; -webkit-line-clamp: 3;
} }
[slot='end'] {
margin-block: auto;
margin-inline-start: 0;
}
stapps-action-chip-list {
float: bottom;
}
ion-item ::ng-deep {
stapps-long-inline-text,
.title {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
white-space: break-spaces;
-webkit-line-clamp: 2;
}
.title-sub {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
margin-block: var(--spacing-xs);
white-space: break-spaces;
-webkit-line-clamp: 1;
}
}
:host.square ::ng-deep { :host.square ::ng-deep {
ion-item { ion-item {
align-items: flex-start;
margin: 0; margin: 0;
} }
@@ -100,12 +74,19 @@ ion-item ::ng-deep {
flex-grow: 0; flex-grow: 0;
} }
stapps-long-inline-text,
.title { .title {
height: 2.5em; overflow: hidden;
display: -webkit-box;
white-space: break-spaces;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
} }
.title ~ :last-child { .title-sub {
margin-right: 42px; display: none;
} }
stapps-rating, stapps-rating,

Some files were not shown because too many files have changed in this diff Show More