From 84f5bc0a01723cb8606096493ee2809f6f5bf1eb Mon Sep 17 00:00:00 2001 From: Dominik Chrastecky Date: Mon, 3 Jun 2024 18:27:30 +0200 Subject: [PATCH] Add (parts of) privacy policy --- src/app/pages/privacy/privacy.component.html | 26 +++- src/app/pages/privacy/privacy.component.scss | 5 + src/app/pages/privacy/privacy.component.ts | 32 +++- src/app/pipes/replace.pipe.ts | 13 ++ src/app/services/data.service.ts | 41 +++++ src/app/types/privacy-policy-item.ts | 11 ++ src/assets/data/privacy.en.json | 148 +++++++++++++++++++ 7 files changed, 272 insertions(+), 4 deletions(-) create mode 100644 src/app/pipes/replace.pipe.ts create mode 100644 src/app/types/privacy-policy-item.ts create mode 100644 src/assets/data/privacy.en.json diff --git a/src/app/pages/privacy/privacy.component.html b/src/app/pages/privacy/privacy.component.html index 5c15d48..d8c93fa 100644 --- a/src/app/pages/privacy/privacy.component.html +++ b/src/app/pages/privacy/privacy.component.html @@ -1 +1,25 @@ -

privacy works!

+@if (policyItems()) { +
+
+
+
+ + @for (sectionPair of policyItems()!|keyvalue:NoSorterKeyValue; track sectionPair) { +

{{sectionPair.key}}

+ + @for (subsectionPair of sectionPair.value|keyvalue:NoSorterKeyValue; track subsectionPair) { + @if (subsectionPair.key) { +

{{subsectionPair.key}}

+ } + @for (item of subsectionPair.value; track item.text) { + + } + } + } +
+
+
+
+} diff --git a/src/app/pages/privacy/privacy.component.scss b/src/app/pages/privacy/privacy.component.scss index e69de29..7469d1f 100644 --- a/src/app/pages/privacy/privacy.component.scss +++ b/src/app/pages/privacy/privacy.component.scss @@ -0,0 +1,5 @@ +h2, h3 { + &:first-of-type { + margin-top: 0; + } +} diff --git a/src/app/pages/privacy/privacy.component.ts b/src/app/pages/privacy/privacy.component.ts index 197fea7..782f41f 100644 --- a/src/app/pages/privacy/privacy.component.ts +++ b/src/app/pages/privacy/privacy.component.ts @@ -1,12 +1,38 @@ -import { Component } from '@angular/core'; +import {Component, OnInit} from '@angular/core'; +import {DataService} from "../../services/data.service"; +import {toPromise} from "../../types/resolvable"; +import {toSignal} from "@angular/core/rxjs-interop"; +import {InlineSvgComponent} from "../../components/inline-svg/inline-svg.component"; +import {TranslocoMarkupComponent} from "ngx-transloco-markup"; +import {FooterColorService} from "../../services/footer-color.service"; +import {KeyValuePipe} from "@angular/common"; +import {NoSorterKeyValue} from "../../types/no-sorter-key-value"; +import {ReplacePipe} from "../../pipes/replace.pipe"; @Component({ selector: 'app-privacy', standalone: true, - imports: [], + imports: [ + InlineSvgComponent, + TranslocoMarkupComponent, + KeyValuePipe, + ReplacePipe + ], templateUrl: './privacy.component.html', styleUrl: './privacy.component.scss' }) -export class PrivacyComponent { +export class PrivacyComponent implements OnInit { + public policyItems = toSignal(this.dataService.privacyPolicy); + constructor( + private readonly dataService: DataService, + private readonly footerColor: FooterColorService, + ) { + } + + public async ngOnInit(): Promise { + this.footerColor.dark.set(true); + } + + protected readonly NoSorterKeyValue = NoSorterKeyValue; } diff --git a/src/app/pipes/replace.pipe.ts b/src/app/pipes/replace.pipe.ts new file mode 100644 index 0000000..e97ab84 --- /dev/null +++ b/src/app/pipes/replace.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'replace', + standalone: true +}) +export class ReplacePipe implements PipeTransform { + + transform(value: string, replaceWhat: string, replaceWith: string): string { + return value.replaceAll(replaceWhat, replaceWith); + } + +} diff --git a/src/app/services/data.service.ts b/src/app/services/data.service.ts index 2f87122..e23dcf3 100644 --- a/src/app/services/data.service.ts +++ b/src/app/services/data.service.ts @@ -5,6 +5,7 @@ import {FaqItem} from "../types/faq-item"; import {TranslocoService} from "@jsverse/transloco"; import {ToolItem} from "../types/tool-item"; import {SortedItems} from "../types/sorted-items"; +import {PrivacyPolicyItem} from "../types/privacy-policy-item"; type ObjectWithMappableKey = TObject[TKey] extends string | null ? TObject : never; @@ -30,6 +31,46 @@ export class DataService { ); } + public get privacyPolicy(): Observable>> { + return this.getData('privacy').pipe( + map (items => { + const sorted = this.formatToMapped(items, 'section'); + const result: Map> = new Map>(); + + sorted.forEach((value, key) => { + const subSorted = this.formatToMapped( + value.map(item => { + const copy = {...item}; + if (copy.context !== undefined) { + for (const key of Object.keys(copy.context)) { + const contextItem = copy.context[key]; + let targetContextValue: string; + switch (contextItem.valueType) { + case "date": + targetContextValue = new Intl.DateTimeFormat(this.transloco.getActiveLang(), { + dateStyle: 'long', + timeStyle: undefined, + }).format(new Date(contextItem.value)); + break; + default: + throw new Error(`Unsupported type: ${contextItem.valueType}`); + } + + copy.text = copy.text.replaceAll(`{${key}}`, targetContextValue); + } + } + return copy; + }), + 'subsection', + ); + result.set(key, subSorted); + }); + + return result; + }), + ); + } + private formatToMapped(objects: ObjectWithMappableKey[], key: TKey): SortedItems { const result: SortedItems = new Map(); for (const object of objects) { diff --git a/src/app/types/privacy-policy-item.ts b/src/app/types/privacy-policy-item.ts new file mode 100644 index 0000000..391a68b --- /dev/null +++ b/src/app/types/privacy-policy-item.ts @@ -0,0 +1,11 @@ +export interface PrivacyPolicyItem { + text: string; + section: string; + subsection: string | null; + context?: { + [key: string]: { + valueType: 'date'; + value: string; + }; + }; +} diff --git a/src/assets/data/privacy.en.json b/src/assets/data/privacy.en.json new file mode 100644 index 0000000..cf9755a --- /dev/null +++ b/src/assets/data/privacy.en.json @@ -0,0 +1,148 @@ +[ + { + "text": "Last updated: {lastUpdatedDate}", + "section": "Privacy Policy", + "subsection": null, + "context": { + "lastUpdatedDate": { + "valueType": "date", + "value": "2024-03-18" + } + } + }, + { + "text": "This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights and how the law protects You.", + "section": "Privacy Policy", + "subsection": null + }, + { + "text": "We use Your Personal data to provide and improve the Service. By using the Service, You agree to the collection and use of information in accordance with this Privacy Policy. This Privacy Policy has been created with the help of the Privacy Policy Generator.", + "section": "Privacy Policy", + "subsection": null + }, + { + "text": "The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural.", + "section": "Interpretation and Definitions", + "subsection": "Interpretation" + }, + { + "text": "For the purposes of this Privacy Policy:", + "section": "Interpretation and Definitions", + "subsection": "Definitions" + }, + { + "text": "{li} Account means a unique account created for You to access our Service or parts of our Service.", + "section": "Interpretation and Definitions", + "subsection": "Definitions" + }, + { + "text": "{li} Company (referred to as either \"the Company\", \"We\", \"Us\" or \"Our\" in this Agreement) refers to AI Horde.", + "section": "Interpretation and Definitions", + "subsection": "Definitions" + }, + { + "text": "{li} Cookies are small files that are placed on Your computer, mobile device or any other device by a website, containing the details of Your browsing history on that website among its many uses.", + "section": "Interpretation and Definitions", + "subsection": "Definitions" + }, + { + "text": "{li} Country refers to: Luxembourg", + "section": "Interpretation and Definitions", + "subsection": "Definitions" + }, + { + "text": "{li} Device means any device that can access the Service such as a computer, a cellphone or a digital tablet.", + "section": "Interpretation and Definitions", + "subsection": "Definitions" + }, + { + "text": "{li} Personal Data is any information that relates to an identified or identifiable individual.", + "section": "Interpretation and Definitions", + "subsection": "Definitions" + }, + { + "text": "{li} Service refers to the Website.", + "section": "Interpretation and Definitions", + "subsection": "Definitions" + }, + { + "text": "{li} Service Provider means any natural or legal person who processes the data on behalf of the Company. It refers to third-party companies or individuals employed by the Company to facilitate the Service, to provide the Service on behalf of the Company, to perform services related to the Service or to assist the Company in analyzing how the Service is used.", + "section": "Interpretation and Definitions", + "subsection": "Definitions" + }, + { + "text": "{li} Usage Data refers to data collected automatically, either generated by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit).", + "section": "Interpretation and Definitions", + "subsection": "Definitions" + }, + { + "text": "{li} Website refers to AI Horde, accessible from https://aihorde.net", + "section": "Interpretation and Definitions", + "subsection": "Definitions" + }, + { + "text": "{li} You means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.", + "section": "Interpretation and Definitions", + "subsection": "Definitions" + }, + { + "text": "{li} Worker means the individual behind a node generating images or text, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.", + "section": "Interpretation and Definitions", + "subsection": "Definitions" + }, + { + "text": "{li} Requestor means the individual providing the prompt reruesting the generation of images or text, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.", + "section": "Interpretation and Definitions", + "subsection": "Definitions" + }, + { + "text": "{li} Prompt refers to the text string sent to generate an image using generative models.", + "section": "Interpretation and Definitions", + "subsection": "Definitions" + }, + { + "text": "", + "section": "Collecting and Using Your Personal Data", + "subsection": null + }, + { + "text": "Our Service does not require any personal data to use it. You are welcome to use it completely anonymously.", + "section": "Types of Data Collected", + "subsection": "Personal Data" + }, + { + "text": "While using Our Service, you may opt-in to provide Us with certain personally identifiable information that can be used to contact or identify You. Personally identifiable information may include, but is not limited to:", + "section": "Types of Data Collected", + "subsection": "Personal Data" + }, + { + "text": "{li} Email address", + "section": "Types of Data Collected", + "subsection": "Personal Data" + }, + { + "text": "Usage Data is collected automatically when using the Service.", + "section": "Types of Data Collected", + "subsection": "Usage Data" + }, + { + "text": "Usage Data may include information such as Your Device's Internet Protocol address (e.g. IP address) and the prompts you used to generate images.", + "section": "Types of Data Collected", + "subsection": "Usage Data" + }, + { + "text": "On an opt-in basis you may provide us with a contact email to use in case of problems with your Workers but this is not used in any other way.", + "section": "Use of Your Personal Data", + "subsection": null + }, + { + "text": "When registering with an oauth2 provider, we maintain a record of the ID number for your provider without any other identifying information. We only use this to connect your oauth provider to your AI Horde account.", + "section": "Use of Your Personal Data", + "subsection": null + }, + { + "text": "The described data storage is necessary for the provision and protection of our online services. The legal basis is Recital 49 of the General Data Protection Regulation (GDPR).", + "section": "Use of Your Personal Data", + "subsection": null + } +]