prettier
This commit is contained in:
1 parent
456a1fffdd
commit
48c6d4b16f
12 files changed
+188
-180
No files matched your search
+28
-18
@@ -5,6 +5,7 @@ This application implements a comprehensive multi-layer image caching strategy f
|
||||
## Caching Layers
|
||||
|
||||
### 1. HTTP Cache Headers (`image-cache.interceptor.ts`)
|
||||
|
||||
- Intercepts HTTP requests for images
|
||||
- Adds `Cache-Control: public, max-age=86400, immutable` headers
|
||||
- Enables browser HTTP cache for 24 hours
|
||||
@@ -12,26 +13,29 @@ This application implements a comprehensive multi-layer image caching strategy f
|
||||
- Specifically targets League of Legends CDN URLs
|
||||
|
||||
### 2. Cache API (`image-preload.service.ts`)
|
||||
|
||||
- Uses browser Cache API for persistent storage
|
||||
- Cache name: `lol-champion-images-v1`
|
||||
- Cache expiry: 7 days
|
||||
- Features:
|
||||
- Preloads images in batches (10 images per batch)
|
||||
- Uses `requestIdleCallback` for non-blocking preload
|
||||
- Automatic cleanup of expired cache entries
|
||||
- Adds custom metadata (`x-cached-date`) to track cache age
|
||||
- Checks cache before fetching from network
|
||||
- Preloads images in batches (10 images per batch)
|
||||
- Uses `requestIdleCallback` for non-blocking preload
|
||||
- Automatic cleanup of expired cache entries
|
||||
- Adds custom metadata (`x-cached-date`) to track cache age
|
||||
- Checks cache before fetching from network
|
||||
|
||||
### 3. Service Worker (Production Only)
|
||||
|
||||
- Angular Service Worker configuration (`ngsw-config.json`)
|
||||
- Two data groups:
|
||||
- **champion-api**: Caches API responses for 1 day
|
||||
- **champion-images**: Caches champion images for 7 days
|
||||
- **champion-api**: Caches API responses for 1 day
|
||||
- **champion-images**: Caches champion images for 7 days
|
||||
- Strategy: `performance` (cache-first, network fallback)
|
||||
- Only active in production builds (`!isDevMode()`)
|
||||
- Registers 30 seconds after app stability
|
||||
|
||||
### 4. Local Storage (`champion-data.service.ts`)
|
||||
|
||||
- Caches champion data (not images) for 24 hours
|
||||
- Prevents unnecessary API calls
|
||||
- Version-controlled cache with automatic invalidation
|
||||
@@ -39,32 +43,36 @@ This application implements a comprehensive multi-layer image caching strategy f
|
||||
## How It Works
|
||||
|
||||
1. **First Load**:
|
||||
- App fetches champion data from API
|
||||
- Data is cached in LocalStorage
|
||||
- Images are preloaded in background using Cache API
|
||||
- HTTP cache headers enable browser caching
|
||||
- App fetches champion data from API
|
||||
- Data is cached in LocalStorage
|
||||
- Images are preloaded in background using Cache API
|
||||
- HTTP cache headers enable browser caching
|
||||
|
||||
2. **Subsequent Loads**:
|
||||
- Champion data loaded from LocalStorage
|
||||
- Images loaded from Cache API (instant)
|
||||
- If cache miss, browser HTTP cache serves images
|
||||
- If HTTP cache miss, network fetch with automatic caching
|
||||
- Champion data loaded from LocalStorage
|
||||
- Images loaded from Cache API (instant)
|
||||
- If cache miss, browser HTTP cache serves images
|
||||
- If HTTP cache miss, network fetch with automatic caching
|
||||
|
||||
3. **Production Builds**:
|
||||
- Service Worker adds additional caching layer
|
||||
- Offline support for previously loaded images
|
||||
- Automatic cache updates on new versions
|
||||
- Service Worker adds additional caching layer
|
||||
- Offline support for previously loaded images
|
||||
- Automatic cache updates on new versions
|
||||
|
||||
## Cache Management
|
||||
|
||||
### Clear Image Cache
|
||||
|
||||
The `ImagePreloadService` provides a `clearCache()` method:
|
||||
|
||||
```typescript
|
||||
inject(ImagePreloadService).clearCache();
|
||||
```
|
||||
|
||||
### Clear Champion Data Cache
|
||||
|
||||
The `ChampionDataService` provides a `clearCache()` method:
|
||||
|
||||
```typescript
|
||||
inject(ChampionDataService).clearCache();
|
||||
```
|
||||
@@ -86,11 +94,13 @@ inject(ChampionDataService).clearCache();
|
||||
## Development vs Production
|
||||
|
||||
**Development** (ng serve):
|
||||
|
||||
- HTTP cache only
|
||||
- Cache API preloading
|
||||
- No service worker
|
||||
|
||||
**Production** (ng build):
|
||||
|
||||
- All caching layers active
|
||||
- Service worker enabled
|
||||
- Maximum performance and offline support
|
||||
|
||||
@@ -5,11 +5,13 @@ This project uses a proxy to avoid CORS issues when fetching champion data from
|
||||
## Architecture
|
||||
|
||||
### Development (`ng serve`)
|
||||
|
||||
- Uses `proxy.conf.json` configuration
|
||||
- Angular CLI dev server handles the proxy
|
||||
- Requests to `/api/champions` are forwarded to the external CDN
|
||||
|
||||
### Production/SSR (`ng build` + `npm run serve:ssr:random-lol`)
|
||||
|
||||
- Uses `http-proxy-middleware` in `src/server.ts`
|
||||
- Express server handles the proxy
|
||||
- Same endpoint `/api/champions` works consistently
|
||||
@@ -31,12 +33,14 @@ This project uses a proxy to avoid CORS issues when fetching champion data from
|
||||
## Testing
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
npm start
|
||||
# Visit http://localhost:4200
|
||||
```
|
||||
|
||||
### Production SSR
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm run serve:ssr:random-lol
|
||||
|
||||
+27
-27
@@ -4,31 +4,31 @@ import prettier from 'eslint-config-prettier';
|
||||
import prettierPlugin from 'eslint-plugin-prettier';
|
||||
|
||||
export default [
|
||||
eslintJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
prettier,
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
plugins: {
|
||||
prettier: prettierPlugin,
|
||||
},
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ['dist/**', 'node_modules/**', '.angular/**', 'coverage/**'],
|
||||
},
|
||||
eslintJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
prettier,
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
plugins: {
|
||||
prettier: prettierPlugin,
|
||||
},
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ['dist/**', 'node_modules/**', '.angular/**', 'coverage/**'],
|
||||
},
|
||||
];
|
||||
@@ -134,7 +134,7 @@ export class ChampionDataService {
|
||||
|
||||
try {
|
||||
const response = await firstValueFrom(
|
||||
this.http.get<Record<string, RemoteChampion>>(CHAMPION_DATA_URL)
|
||||
this.http.get<Record<string, RemoteChampion>>(CHAMPION_DATA_URL),
|
||||
);
|
||||
|
||||
const champions = Object.entries(response)
|
||||
@@ -174,8 +174,8 @@ export class ChampionDataService {
|
||||
new Set(
|
||||
data.positions
|
||||
.map((position) => POSITION_TO_LANE[position])
|
||||
.filter((role): role is Lane => role !== undefined)
|
||||
)
|
||||
.filter((role): role is Lane => role !== undefined),
|
||||
),
|
||||
);
|
||||
|
||||
if (roles.length === 0 || !data.icon) {
|
||||
|
||||
@@ -16,85 +16,89 @@
|
||||
<span>Each roll keeps champions unique across every lane.</span>
|
||||
</div>
|
||||
@if (isLoading()) {
|
||||
<div class="status mat-body-small" aria-live="polite">
|
||||
<mat-icon aria-hidden="true">autorenew</mat-icon>
|
||||
<span>Loading champion data…</span>
|
||||
</div>
|
||||
<div class="status mat-body-small" aria-live="polite">
|
||||
<mat-icon aria-hidden="true">autorenew</mat-icon>
|
||||
<span>Loading champion data…</span>
|
||||
</div>
|
||||
} @else if (loadError(); as errorMessage) {
|
||||
<div class="status error mat-body-small" aria-live="polite">
|
||||
<mat-icon aria-hidden="true" color="warn">error</mat-icon>
|
||||
<span>{{ errorMessage }}</span>
|
||||
<button type="button" mat-button color="accent" (click)="retryLoad()">
|
||||
<mat-icon aria-hidden="true">refresh</mat-icon>
|
||||
<span>Retry</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="status error mat-body-small" aria-live="polite">
|
||||
<mat-icon aria-hidden="true" color="warn">error</mat-icon>
|
||||
<span>{{ errorMessage }}</span>
|
||||
<button type="button" mat-button color="accent" (click)="retryLoad()">
|
||||
<mat-icon aria-hidden="true">refresh</mat-icon>
|
||||
<span>Retry</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
<div class="lane-grid">
|
||||
@for (assignment of laneAssignments(); track assignment.lane) {
|
||||
<mat-card class="lane-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ laneLabel(assignment.lane) }}</mat-card-title>
|
||||
<mat-card-subtitle>
|
||||
{{
|
||||
assignment.champion
|
||||
? 'Unique pick locked in'
|
||||
: 'Champion needed for this lane'
|
||||
}}
|
||||
</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
@if (assignment.champion; as champion) {
|
||||
<div class="champion-details">
|
||||
<img
|
||||
[ngSrc]="champion.icon"
|
||||
width="96"
|
||||
height="96"
|
||||
[class.loading-image]="isLoading()"
|
||||
[alt]="champion.name + ' portrait'"
|
||||
[priority]="true"
|
||||
class="champion-icon"
|
||||
/>
|
||||
<p class="champion-name mat-title-medium">{{ champion.name }}</p>
|
||||
</div>
|
||||
<mat-chip-set class="roles-set" [attr.aria-label]="'Roles for ' + champion.name">
|
||||
@for (role of champion.roles; track role) {
|
||||
<mat-chip>{{ roleLabel(role) }}</mat-chip>
|
||||
<mat-card class="lane-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ laneLabel(assignment.lane) }}</mat-card-title>
|
||||
<mat-card-subtitle>
|
||||
{{
|
||||
assignment.champion
|
||||
? 'Unique pick locked in'
|
||||
: 'Champion needed for this lane'
|
||||
}}
|
||||
</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
@if (assignment.champion; as champion) {
|
||||
<div class="champion-details">
|
||||
<img
|
||||
[ngSrc]="champion.icon"
|
||||
width="96"
|
||||
height="96"
|
||||
[class.loading-image]="isLoading()"
|
||||
[alt]="champion.name + ' portrait'"
|
||||
[priority]="true"
|
||||
class="champion-icon"
|
||||
/>
|
||||
<p class="champion-name mat-title-medium">{{ champion.name }}</p>
|
||||
</div>
|
||||
<mat-chip-set
|
||||
class="roles-set"
|
||||
[attr.aria-label]="'Roles for ' + champion.name"
|
||||
>
|
||||
@for (role of champion.roles; track role) {
|
||||
<mat-chip>{{ roleLabel(role) }}</mat-chip>
|
||||
}
|
||||
</mat-chip-set>
|
||||
} @else {
|
||||
<p class="champion-name champion-name-empty mat-title-medium">
|
||||
No champion available
|
||||
</p>
|
||||
<p class="champion-roles mat-body-small">
|
||||
Roll again once champions finish loading.
|
||||
</p>
|
||||
}
|
||||
</mat-chip-set>
|
||||
} @else {
|
||||
<p class="champion-name champion-name-empty mat-title-medium">
|
||||
No champion available
|
||||
</p>
|
||||
<p class="champion-roles mat-body-small">
|
||||
Roll again once champions finish loading.
|
||||
</p>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
matFab
|
||||
extended
|
||||
color="accent"
|
||||
class="change-champion-button"
|
||||
[disabled]="isLoading() || !hasChampions()"
|
||||
(click)="changeChampion(assignment.lane)"
|
||||
>
|
||||
<mat-icon aria-hidden="true">refresh</mat-icon>
|
||||
Re-Roll ?
|
||||
</button>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<button
|
||||
type="button"
|
||||
matFab
|
||||
extended
|
||||
color="accent"
|
||||
class="change-champion-button"
|
||||
[disabled]="isLoading() || !hasChampions()"
|
||||
(click)="changeChampion(assignment.lane)"
|
||||
>
|
||||
<mat-icon aria-hidden="true">refresh</mat-icon>
|
||||
Re-Roll ?
|
||||
</button>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
@if (hasMissingChampion()) {
|
||||
<mat-card class="warning-card">
|
||||
<mat-card-content>
|
||||
<mat-icon aria-hidden="true" color="warn">warning</mat-icon>
|
||||
<span>
|
||||
Unable to assign a unique champion to every lane. Add more champions to the pool.
|
||||
</span>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="warning-card">
|
||||
<mat-card-content>
|
||||
<mat-icon aria-hidden="true" color="warn">warning</mat-icon>
|
||||
<span>
|
||||
Unable to assign a unique champion to every lane. Add more champions to the
|
||||
pool.
|
||||
</span>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</section>
|
||||
@@ -55,7 +55,7 @@ export class ChampionRandomizerComponent {
|
||||
protected readonly hasChampions = computed(() => this.champions().length > 0);
|
||||
|
||||
protected readonly assignments = signal<Record<Lane, Champion | null>>(
|
||||
this.createEmptyAssignments()
|
||||
this.createEmptyAssignments(),
|
||||
);
|
||||
|
||||
protected readonly usedChampions = computed<Record<Lane, Champion | null>>(() => {
|
||||
@@ -111,11 +111,11 @@ export class ChampionRandomizerComponent {
|
||||
champion,
|
||||
rolesText,
|
||||
} satisfies LaneAssignmentView;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
protected readonly hasMissingChampion = computed(() =>
|
||||
this.lanes.some((lane) => this.assignments()[lane] === null)
|
||||
this.lanes.some((lane) => this.assignments()[lane] === null),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
@@ -154,7 +154,7 @@ export class ChampionRandomizerComponent {
|
||||
|
||||
for (const lane of this.lanes) {
|
||||
const candidates = (championsByLaneMap.get(lane) || []).filter(
|
||||
(champion) => !selected.has(champion.name)
|
||||
(champion) => !selected.has(champion.name),
|
||||
);
|
||||
|
||||
if (candidates.length === 0) {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
+2
-2
@@ -36,7 +36,7 @@ app.use(
|
||||
pathRewrite: {
|
||||
'^.*': '',
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -47,7 +47,7 @@ app.use(
|
||||
maxAge: '1y',
|
||||
index: false,
|
||||
redirect: false,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -7,5 +7,5 @@ body {
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||
font-family: Roboto, 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
+7
-13
@@ -1,17 +1,11 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/*.spec.ts"]
|
||||
}
|
||||
+30
-30
@@ -1,34 +1,34 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
+6
-10
@@ -1,14 +1,10 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": ["jasmine"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in new issue
Block a user