From f4cc2838e7a55cd95426a7837f9208219224643a Mon Sep 17 00:00:00 2001 From: Sverre <59171289+sverben@users.noreply.github.com> Date: Wed, 24 Apr 2024 12:22:09 +0200 Subject: [PATCH 1/2] chore: add prettier (#3) --- .github/workflows/pull-request.yml | 25 + App.tsx | 164 +++--- package.json | 1 + src/__generated__/media.ts | 902 +++++++++++++++-------------- src/auth.tsx | 597 ++++++++++--------- src/components/feed/item.tsx | 87 +-- src/components/media/preview.tsx | 53 +- src/components/navbar.tsx | 29 +- src/screens/feed/item.tsx | 46 +- src/screens/feed/search.tsx | 246 ++++---- src/screens/media/album.tsx | 255 ++++---- src/screens/media/media.tsx | 116 ++-- src/screens/media/slides.tsx | 112 ++-- src/screens/settings.tsx | 89 +-- src/screens/web.tsx | 46 +- src/stores/media.ts | 28 +- yarn.lock | 5 + 17 files changed, 1513 insertions(+), 1288 deletions(-) create mode 100644 .github/workflows/pull-request.yml diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..f32177a --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,25 @@ +name: Validate a pull request +on: + pull_request: + branches: + - main + workflow_dispatch: {} + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: "20.x" + registry-url: "https://registry.npmjs.org" + + - name: Install packages + run: yarn install --frozen-lockfile + + - name: Run prettier + run: yarn prettier -c ./src App.tsx diff --git a/App.tsx b/App.tsx index 408ab3d..827600b 100644 --- a/App.tsx +++ b/App.tsx @@ -1,92 +1,126 @@ -import {adaptNavigationTheme, MD3DarkTheme, MD3LightTheme, PaperProvider} from "react-native-paper"; -import {AuthProvider} from "./src/auth"; -import {decode, encode} from "base-64"; import { - DarkTheme as NavigationDarkTheme, - DefaultTheme as NavigationDefaultTheme, - NavigationContainer + adaptNavigationTheme, + MD3DarkTheme, + MD3LightTheme, + PaperProvider, +} from "react-native-paper"; +import { AuthProvider } from "./src/auth"; +import { decode, encode } from "base-64"; +import { + DarkTheme as NavigationDarkTheme, + DefaultTheme as NavigationDefaultTheme, + NavigationContainer, } from "@react-navigation/native"; import HomeScreen from "./src/screens/home"; import CustomNavigationBar from "./src/components/navbar"; import SlotScreen from "./src/screens/feed/slot"; -import {createStackNavigator} from "@react-navigation/stack"; +import { createStackNavigator } from "@react-navigation/stack"; import AlbumScreen from "./src/screens/media/album"; -import {Item} from "./src/__generated__/media"; +import { Item } from "./src/__generated__/media"; import SlidesScreen from "./src/screens/media/slides"; -import {useColorScheme} from "react-native"; -import merge from 'deepmerge' +import { useColorScheme } from "react-native"; +import merge from "deepmerge"; import WebScreen from "./src/screens/web"; -import * as Notifications from 'expo-notifications' +import * as Notifications from "expo-notifications"; import SearchScreen from "./src/screens/feed/search"; -import { Item as InventoryItem } from './src/screens/feed/search' +import { Item as InventoryItem } from "./src/screens/feed/search"; import ItemScreen from "./src/screens/feed/item"; -Notifications.setNotificationHandler(({ - handleNotification: async () => ({ - shouldShowAlert: true, - shouldPlaySound: false, - shouldSetBadge: true - }) -})) +Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldShowAlert: true, + shouldPlaySound: false, + shouldSetBadge: true, + }), +}); if (!global.btoa) global.btoa = encode; if (!global.atob) global.atob = decode; export type StackParamList = { - Home: undefined, - Slot: { slot: number, title: string }, - Album: { album: string, title: string }, - Slides: { items: Item[], item: number }, - Web: { source: string, title: string }, - Search: undefined, - Item: { item: InventoryItem, title: string } -} - + Home: undefined; + Slot: { slot: number; title: string }; + Album: { album: string; title: string }; + Slides: { items: Item[]; item: number }; + Web: { source: string; title: string }; + Search: undefined; + Item: { item: InventoryItem; title: string }; +}; -const Stack = createStackNavigator() +const Stack = createStackNavigator(); const { LightTheme, DarkTheme } = adaptNavigationTheme({ - reactNavigationLight: NavigationDefaultTheme, - reactNavigationDark: NavigationDarkTheme, + reactNavigationLight: NavigationDefaultTheme, + reactNavigationDark: NavigationDarkTheme, }); const CombinedDefaultTheme = merge(MD3LightTheme, LightTheme); const CombinedDarkTheme = merge(MD3DarkTheme, DarkTheme); export default function App() { - const colorScheme = useColorScheme() + const colorScheme = useColorScheme(); - return ( - - - - - - ({ - title: route.params.title, - })} /> - ({ - title: route.params.title - })} /> - - ({ - title: route.params.title - })} /> - - ({ - title: route.params.title - })} /> - - - - - ); + return ( + + + + + + ({ + title: route.params.title, + })} + /> + ({ + title: route.params.title, + })} + /> + + ({ + title: route.params.title, + })} + /> + + ({ + title: route.params.title, + })} + /> + + + + + ); } diff --git a/package.json b/package.json index a41bfaa..abc3601 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@types/fast-html-parser": "^1.0.4", "@types/react": "~18.2.45", "@types/react-native-vector-icons": "^6.4.18", + "prettier": "^3.2.5", "typescript": "^5.1.3" }, "private": true diff --git a/src/__generated__/media.ts b/src/__generated__/media.ts index c0b83bc..1875cec 100644 --- a/src/__generated__/media.ts +++ b/src/__generated__/media.ts @@ -11,503 +11,551 @@ /** Album */ export interface Album { - /** Name */ - name: string; - /** Description */ - description: string; - /** - * Id - * @format uuid - */ - id: string; - /** Items */ - items: Item[]; - /** Order */ - order: number; - preview: Item | null; + /** Name */ + name: string; + /** Description */ + description: string; + /** + * Id + * @format uuid + */ + id: string; + /** Items */ + items: Item[]; + /** Order */ + order: number; + preview: Item | null; } /** AlbumCreate */ export interface AlbumCreate { - /** Name */ - name: string; - /** Description */ - description: string; + /** Name */ + name: string; + /** Description */ + description: string; } /** AlbumList */ export interface AlbumList { - /** Name */ - name: string; - /** Description */ - description: string; - /** - * Id - * @format uuid - */ - id: string; - /** Order */ - order: number; - preview: Item | null; + /** Name */ + name: string; + /** Description */ + description: string; + /** + * Id + * @format uuid + */ + id: string; + /** Order */ + order: number; + preview: Item | null; } /** AlbumOrder */ export interface AlbumOrder { - /** - * Id - * @format uuid - */ - id: string; - /** Order */ - order: number; + /** + * Id + * @format uuid + */ + id: string; + /** Order */ + order: number; } /** Body_upload_items */ export interface BodyUploadItems { - /** Items */ - items: File[]; + /** Items */ + items: File[]; } /** HTTPValidationError */ export interface HTTPValidationError { - /** Detail */ - detail?: ValidationError[]; + /** Detail */ + detail?: ValidationError[]; } /** Item */ export interface Item { - /** - * Id - * @format uuid - */ - id: string; - /** - * Date - * @format date-time - */ - date: string; - /** Width */ - width: number; - /** Height */ - height: number; - /** Type */ - type: number; - /** User */ - user: string; - /** Path */ - path: string; - /** Cover Path */ - cover_path: string; + /** + * Id + * @format uuid + */ + id: string; + /** + * Date + * @format date-time + */ + date: string; + /** Width */ + width: number; + /** Height */ + height: number; + /** Type */ + type: number; + /** User */ + user: string; + /** Path */ + path: string; + /** Cover Path */ + cover_path: string; } /** User */ export interface User { - /** Id */ - id: string; - /** Admin */ - admin: boolean; + /** Id */ + id: string; + /** Admin */ + admin: boolean; } /** ValidationError */ export interface ValidationError { - /** Location */ - loc: (string | number)[]; - /** Message */ - msg: string; - /** Error Type */ - type: string; + /** Location */ + loc: (string | number)[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; } export type QueryParamsType = Record; export type ResponseFormat = keyof Omit; export interface FullRequestParams extends Omit { - /** set parameter to `true` for call `securityWorker` for this request */ - secure?: boolean; - /** request path */ - path: string; - /** content type of request body */ - type?: ContentType; - /** query params */ - query?: QueryParamsType; - /** format of response (i.e. response.json() -> format: "json") */ - format?: ResponseFormat; - /** request body */ - body?: unknown; - /** base url */ - baseUrl?: string; - /** request cancellation token */ - cancelToken?: CancelToken; + /** set parameter to `true` for call `securityWorker` for this request */ + secure?: boolean; + /** request path */ + path: string; + /** content type of request body */ + type?: ContentType; + /** query params */ + query?: QueryParamsType; + /** format of response (i.e. response.json() -> format: "json") */ + format?: ResponseFormat; + /** request body */ + body?: unknown; + /** base url */ + baseUrl?: string; + /** request cancellation token */ + cancelToken?: CancelToken; } -export type RequestParams = Omit; +export type RequestParams = Omit< + FullRequestParams, + "body" | "method" | "query" | "path" +>; export interface ApiConfig { - baseUrl?: string; - baseApiParams?: Omit; - securityWorker?: (securityData: SecurityDataType | null) => Promise | RequestParams | void; - customFetch?: typeof fetch; + baseUrl?: string; + baseApiParams?: Omit; + securityWorker?: ( + securityData: SecurityDataType | null, + ) => Promise | RequestParams | void; + customFetch?: typeof fetch; } -export interface HttpResponse extends Response { - data: D; - error: E; +export interface HttpResponse + extends Response { + data: D; + error: E; } type CancelToken = Symbol | string | number; export enum ContentType { - Json = "application/json", - FormData = "multipart/form-data", - UrlEncoded = "application/x-www-form-urlencoded", - Text = "text/plain", + Json = "application/json", + FormData = "multipart/form-data", + UrlEncoded = "application/x-www-form-urlencoded", + Text = "text/plain", } export class HttpClient { - public baseUrl: string = ""; - private securityData: SecurityDataType | null = null; - private securityWorker?: ApiConfig["securityWorker"]; - private abortControllers = new Map(); - private customFetch = (...fetchParams: Parameters) => fetch(...fetchParams); - - private baseApiParams: RequestParams = { - credentials: "same-origin", - headers: {}, - redirect: "follow", - referrerPolicy: "no-referrer", - }; - - constructor(apiConfig: ApiConfig = {}) { - Object.assign(this, apiConfig); - } - - public setSecurityData = (data: SecurityDataType | null) => { - this.securityData = data; + public baseUrl: string = ""; + private securityData: SecurityDataType | null = null; + private securityWorker?: ApiConfig["securityWorker"]; + private abortControllers = new Map(); + private customFetch = (...fetchParams: Parameters) => + fetch(...fetchParams); + + private baseApiParams: RequestParams = { + credentials: "same-origin", + headers: {}, + redirect: "follow", + referrerPolicy: "no-referrer", + }; + + constructor(apiConfig: ApiConfig = {}) { + Object.assign(this, apiConfig); + } + + public setSecurityData = (data: SecurityDataType | null) => { + this.securityData = data; + }; + + protected encodeQueryParam(key: string, value: any) { + const encodedKey = encodeURIComponent(key); + return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`; + } + + protected addQueryParam(query: QueryParamsType, key: string) { + return this.encodeQueryParam(key, query[key]); + } + + protected addArrayQueryParam(query: QueryParamsType, key: string) { + const value = query[key]; + return value.map((v: any) => this.encodeQueryParam(key, v)).join("&"); + } + + protected toQueryString(rawQuery?: QueryParamsType): string { + const query = rawQuery || {}; + const keys = Object.keys(query).filter( + (key) => "undefined" !== typeof query[key], + ); + return keys + .map((key) => + Array.isArray(query[key]) + ? this.addArrayQueryParam(query, key) + : this.addQueryParam(query, key), + ) + .join("&"); + } + + protected addQueryParams(rawQuery?: QueryParamsType): string { + const queryString = this.toQueryString(rawQuery); + return queryString ? `?${queryString}` : ""; + } + + private contentFormatters: Record any> = { + [ContentType.Json]: (input: any) => + input !== null && (typeof input === "object" || typeof input === "string") + ? JSON.stringify(input) + : input, + [ContentType.Text]: (input: any) => + input !== null && typeof input !== "string" + ? JSON.stringify(input) + : input, + [ContentType.FormData]: (input: any) => + Object.keys(input || {}).reduce((formData, key) => { + const property = input[key]; + formData.append( + key, + property instanceof Blob + ? property + : typeof property === "object" && property !== null + ? JSON.stringify(property) + : `${property}`, + ); + return formData; + }, new FormData()), + [ContentType.UrlEncoded]: (input: any) => this.toQueryString(input), + }; + + protected mergeRequestParams( + params1: RequestParams, + params2?: RequestParams, + ): RequestParams { + return { + ...this.baseApiParams, + ...params1, + ...(params2 || {}), + headers: { + ...(this.baseApiParams.headers || {}), + ...(params1.headers || {}), + ...((params2 && params2.headers) || {}), + }, }; - - protected encodeQueryParam(key: string, value: any) { - const encodedKey = encodeURIComponent(key); - return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`; - } - - protected addQueryParam(query: QueryParamsType, key: string) { - return this.encodeQueryParam(key, query[key]); - } - - protected addArrayQueryParam(query: QueryParamsType, key: string) { - const value = query[key]; - return value.map((v: any) => this.encodeQueryParam(key, v)).join("&"); + } + + protected createAbortSignal = ( + cancelToken: CancelToken, + ): AbortSignal | undefined => { + if (this.abortControllers.has(cancelToken)) { + const abortController = this.abortControllers.get(cancelToken); + if (abortController) { + return abortController.signal; + } + return void 0; } - protected toQueryString(rawQuery?: QueryParamsType): string { - const query = rawQuery || {}; - const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]); - return keys - .map((key) => (Array.isArray(query[key]) ? this.addArrayQueryParam(query, key) : this.addQueryParam(query, key))) - .join("&"); - } + const abortController = new AbortController(); + this.abortControllers.set(cancelToken, abortController); + return abortController.signal; + }; - protected addQueryParams(rawQuery?: QueryParamsType): string { - const queryString = this.toQueryString(rawQuery); - return queryString ? `?${queryString}` : ""; - } + public abortRequest = (cancelToken: CancelToken) => { + const abortController = this.abortControllers.get(cancelToken); - private contentFormatters: Record any> = { - [ContentType.Json]: (input: any) => - input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input, - [ContentType.Text]: (input: any) => (input !== null && typeof input !== "string" ? JSON.stringify(input) : input), - [ContentType.FormData]: (input: any) => - Object.keys(input || {}).reduce((formData, key) => { - const property = input[key]; - formData.append( - key, - property instanceof Blob - ? property - : typeof property === "object" && property !== null - ? JSON.stringify(property) - : `${property}`, - ); - return formData; - }, new FormData()), - [ContentType.UrlEncoded]: (input: any) => this.toQueryString(input), - }; - - protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams { - return { - ...this.baseApiParams, - ...params1, - ...(params2 || {}), - headers: { - ...(this.baseApiParams.headers || {}), - ...(params1.headers || {}), - ...((params2 && params2.headers) || {}), - }, - }; + if (abortController) { + abortController.abort(); + this.abortControllers.delete(cancelToken); } - - protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => { - if (this.abortControllers.has(cancelToken)) { - const abortController = this.abortControllers.get(cancelToken); - if (abortController) { - return abortController.signal; - } - return void 0; - } - - const abortController = new AbortController(); - this.abortControllers.set(cancelToken, abortController); - return abortController.signal; - }; - - public abortRequest = (cancelToken: CancelToken) => { - const abortController = this.abortControllers.get(cancelToken); - - if (abortController) { - abortController.abort(); - this.abortControllers.delete(cancelToken); - } - }; - - public request = async ({ - body, - secure, - path, - type, - query, - format, - baseUrl, - cancelToken, - ...params - }: FullRequestParams): Promise> => { - const secureParams = - ((typeof secure === "boolean" ? secure : this.baseApiParams.secure) && - this.securityWorker && - (await this.securityWorker(this.securityData))) || - {}; - const requestParams = this.mergeRequestParams(params, secureParams); - const queryString = query && this.toQueryString(query); - const payloadFormatter = this.contentFormatters[type || ContentType.Json]; - const responseFormat = format || requestParams.format; - - return this.customFetch(`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`, { - ...requestParams, - headers: { - ...(requestParams.headers || {}), - ...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}), - }, - signal: (cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal) || null, - body: typeof body === "undefined" || body === null ? null : payloadFormatter(body), - }).then(async (response) => { - const r = response as HttpResponse; - r.data = null as unknown as T; - r.error = null as unknown as E; - - const data = !responseFormat - ? r - : await response[responseFormat]() - .then((data) => { - if (r.ok) { - r.data = data; - } else { - r.error = data; - } - return r; - }) - .catch((e) => { - r.error = e; - return r; - }); - - if (cancelToken) { - this.abortControllers.delete(cancelToken); - } - - if (!response.ok) throw data; - return data; - }); - }; + }; + + public request = async ({ + body, + secure, + path, + type, + query, + format, + baseUrl, + cancelToken, + ...params + }: FullRequestParams): Promise> => { + const secureParams = + ((typeof secure === "boolean" ? secure : this.baseApiParams.secure) && + this.securityWorker && + (await this.securityWorker(this.securityData))) || + {}; + const requestParams = this.mergeRequestParams(params, secureParams); + const queryString = query && this.toQueryString(query); + const payloadFormatter = this.contentFormatters[type || ContentType.Json]; + const responseFormat = format || requestParams.format; + + return this.customFetch( + `${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`, + { + ...requestParams, + headers: { + ...(requestParams.headers || {}), + ...(type && type !== ContentType.FormData + ? { "Content-Type": type } + : {}), + }, + signal: + (cancelToken + ? this.createAbortSignal(cancelToken) + : requestParams.signal) || null, + body: + typeof body === "undefined" || body === null + ? null + : payloadFormatter(body), + }, + ).then(async (response) => { + const r = response as HttpResponse; + r.data = null as unknown as T; + r.error = null as unknown as E; + + const data = !responseFormat + ? r + : await response[responseFormat]() + .then((data) => { + if (r.ok) { + r.data = data; + } else { + r.error = data; + } + return r; + }) + .catch((e) => { + r.error = e; + return r; + }); + + if (cancelToken) { + this.abortControllers.delete(cancelToken); + } + + if (!response.ok) throw data; + return data; + }); + }; } /** * @title FastAPI * @version 0.1.0 */ -export class Api extends HttpClient { - albums = { - /** - * No description - * - * @name GetAlbums - * @summary Get Albums - * @request GET:/albums - * @secure - */ - getAlbums: (params: RequestParams = {}) => - this.request({ - path: `/albums`, - method: "GET", - secure: true, - format: "json", - ...params, - }), +export class Api< + SecurityDataType extends unknown, +> extends HttpClient { + albums = { + /** + * No description + * + * @name GetAlbums + * @summary Get Albums + * @request GET:/albums + * @secure + */ + getAlbums: (params: RequestParams = {}) => + this.request({ + path: `/albums`, + method: "GET", + secure: true, + format: "json", + ...params, + }), - /** - * No description - * - * @name CreateAlbum - * @summary Create Album - * @request POST:/albums - * @secure - */ - createAlbum: (data: AlbumCreate, params: RequestParams = {}) => - this.request({ - path: `/albums`, - method: "POST", - body: data, - secure: true, - type: ContentType.Json, - format: "json", - ...params, - }), + /** + * No description + * + * @name CreateAlbum + * @summary Create Album + * @request POST:/albums + * @secure + */ + createAlbum: (data: AlbumCreate, params: RequestParams = {}) => + this.request({ + path: `/albums`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), - /** - * No description - * - * @name OrderAlbums - * @summary Order Albums - * @request PATCH:/albums - * @secure - */ - orderAlbums: (data: AlbumOrder[], params: RequestParams = {}) => - this.request({ - path: `/albums`, - method: "PATCH", - body: data, - secure: true, - type: ContentType.Json, - format: "json", - ...params, - }), + /** + * No description + * + * @name OrderAlbums + * @summary Order Albums + * @request PATCH:/albums + * @secure + */ + orderAlbums: (data: AlbumOrder[], params: RequestParams = {}) => + this.request({ + path: `/albums`, + method: "PATCH", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), - /** - * No description - * - * @name GetAlbum - * @summary Get Album - * @request GET:/albums/{album_id} - * @secure - */ - getAlbum: (albumId: string, params: RequestParams = {}) => - this.request({ - path: `/albums/${albumId}`, - method: "GET", - secure: true, - format: "json", - ...params, - }), + /** + * No description + * + * @name GetAlbum + * @summary Get Album + * @request GET:/albums/{album_id} + * @secure + */ + getAlbum: (albumId: string, params: RequestParams = {}) => + this.request({ + path: `/albums/${albumId}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), - /** - * No description - * - * @name UpdateAlbum - * @summary Update Album - * @request PATCH:/albums/{album_id} - * @secure - */ - updateAlbum: (albumId: string, data: AlbumCreate, params: RequestParams = {}) => - this.request({ - path: `/albums/${albumId}`, - method: "PATCH", - body: data, - secure: true, - type: ContentType.Json, - format: "json", - ...params, - }), + /** + * No description + * + * @name UpdateAlbum + * @summary Update Album + * @request PATCH:/albums/{album_id} + * @secure + */ + updateAlbum: ( + albumId: string, + data: AlbumCreate, + params: RequestParams = {}, + ) => + this.request({ + path: `/albums/${albumId}`, + method: "PATCH", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + /** + * No description + * + * @name SetPreview + * @summary Set Preview + * @request POST:/albums/{album_id}/preview + * @secure + */ + setPreview: ( + albumId: string, + query: { /** - * No description - * - * @name SetPreview - * @summary Set Preview - * @request POST:/albums/{album_id}/preview - * @secure - */ - setPreview: ( - albumId: string, - query: { - /** - * Item Id - * @format uuid - */ - item_id: string; - }, - params: RequestParams = {}, - ) => - this.request({ - path: `/albums/${albumId}/preview`, - method: "POST", - query: query, - secure: true, - format: "json", - ...params, - }), - }; - items = { - /** - * No description - * - * @name UploadItems - * @summary Upload Items - * @request POST:/items/{album_id} - * @secure + * Item Id + * @format uuid */ - uploadItems: (albumId: string, data: BodyUploadItems, params: RequestParams = {}) => - this.request({ - path: `/items/${albumId}`, - method: "POST", - body: data, - secure: true, - type: ContentType.FormData, - format: "json", - ...params, - }), + item_id: string; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/albums/${albumId}/preview`, + method: "POST", + query: query, + secure: true, + format: "json", + ...params, + }), + }; + items = { + /** + * No description + * + * @name UploadItems + * @summary Upload Items + * @request POST:/items/{album_id} + * @secure + */ + uploadItems: ( + albumId: string, + data: BodyUploadItems, + params: RequestParams = {}, + ) => + this.request({ + path: `/items/${albumId}`, + method: "POST", + body: data, + secure: true, + type: ContentType.FormData, + format: "json", + ...params, + }), - /** - * No description - * - * @name DeleteItems - * @summary Delete Items - * @request POST:/items/{album_id}/delete - * @secure - */ - deleteItems: (albumId: string, data: string[], params: RequestParams = {}) => - this.request({ - path: `/items/${albumId}/delete`, - method: "POST", - body: data, - secure: true, - type: ContentType.Json, - format: "json", - ...params, - }), - }; - users = { - /** - * No description - * - * @name GetUser - * @summary Get User - * @request GET:/users/me - * @secure - */ - getUser: (params: RequestParams = {}) => - this.request({ - path: `/users/me`, - method: "GET", - secure: true, - format: "json", - ...params, - }), - }; + /** + * No description + * + * @name DeleteItems + * @summary Delete Items + * @request POST:/items/{album_id}/delete + * @secure + */ + deleteItems: ( + albumId: string, + data: string[], + params: RequestParams = {}, + ) => + this.request({ + path: `/items/${albumId}/delete`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + }; + users = { + /** + * No description + * + * @name GetUser + * @summary Get User + * @request GET:/users/me + * @secure + */ + getUser: (params: RequestParams = {}) => + this.request({ + path: `/users/me`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + }; } diff --git a/src/auth.tsx b/src/auth.tsx index 2f62f62..fff79c8 100644 --- a/src/auth.tsx +++ b/src/auth.tsx @@ -1,320 +1,365 @@ -import {createContext, ReactNode, useState, JSX, useEffect} from 'react' +import { createContext, ReactNode, useState, JSX, useEffect } from "react"; import { jwtDecode } from "jwt-decode"; -import * as SecureStore from 'expo-secure-store' +import * as SecureStore from "expo-secure-store"; import * as AuthSession from "expo-auth-session"; -import {Alert, Platform, SafeAreaView, StyleSheet, View} from "react-native"; -import {Button, useTheme, Text} from "react-native-paper"; +import { Alert, Platform, SafeAreaView, StyleSheet, View } from "react-native"; +import { Button, useTheme, Text } from "react-native-paper"; import * as WebBrowser from "expo-web-browser"; -import {DiscoveryDocument, TokenError} from "expo-auth-session"; -import * as Device from 'expo-device'; +import { DiscoveryDocument, TokenError } from "expo-auth-session"; +import * as Device from "expo-device"; import * as Notifications from "expo-notifications"; -import Constants from 'expo-constants' +import Constants from "expo-constants"; -const redirectUri = AuthSession.makeRedirectUri({ path: 'redirect' }) -WebBrowser.maybeCompleteAuthSession() +const redirectUri = AuthSession.makeRedirectUri({ path: "redirect" }); +WebBrowser.maybeCompleteAuthSession(); export enum Authed { - LOADING, - UNAUTHENTICATED, - GUEST, - AUTHENTICATED, + LOADING, + UNAUTHENTICATED, + GUEST, + AUTHENTICATED, } interface User { - aud: string - iat: number - at_hash: string - sub: string - given_name: string - family_name: string - email: string - aanmelden: boolean - media: boolean - account_type: string - days: number - iss: string - exp: number - auth_time: number - jti: string + aud: string; + iat: number; + at_hash: string; + sub: string; + given_name: string; + family_name: string; + email: string; + aanmelden: boolean; + media: boolean; + account_type: string; + days: number; + iss: string; + exp: number; + auth_time: number; + jti: string; } interface LoadingState { - authenticated: Authed.LOADING + authenticated: Authed.LOADING; } interface AuthenticatedState { - authenticated: Authed.AUTHENTICATED - user: User - token: Promise, - logout: () => Promise + authenticated: Authed.AUTHENTICATED; + user: User; + token: Promise; + logout: () => Promise; } interface UnAuthenticatedState { - authenticated: Authed.UNAUTHENTICATED + authenticated: Authed.UNAUTHENTICATED; } interface GuestState { - authenticated: Authed.GUEST, - login: () => void + authenticated: Authed.GUEST; + login: () => void; } -type AuthState = AuthenticatedState | UnAuthenticatedState | LoadingState | GuestState +type AuthState = + | AuthenticatedState + | UnAuthenticatedState + | LoadingState + | GuestState; const AuthContext = createContext({ - authenticated: Authed.LOADING -}) + authenticated: Authed.LOADING, +}); interface TokenResponse { - id_token: string - refresh_token?: string - expires_in: number + id_token: string; + refresh_token?: string; + expires_in: number; } async function registerForPushNotifications(user: AuthenticatedState) { - if (Platform.OS === 'android') { - await Notifications.setNotificationChannelAsync('default', { - name: 'default', - importance: Notifications.AndroidImportance.DEFAULT, - vibrationPattern: [0, 250, 250, 250], - lightColor: '#FF231F7C', - }) - } - - if (!Device.isDevice) return - - const { status: existingStatus } = await Notifications.getPermissionsAsync() - let finalStatus = existingStatus - if (existingStatus !== 'granted') { - const { status } = await Notifications.requestPermissionsAsync() - finalStatus = status - } - if (finalStatus !== 'granted') { - return - } - - const config = Constants.expoConfig - if (!config) return - - const { data: pushToken } = await Notifications.getExpoPushTokenAsync({ projectId: config.extra?.eas.projectId }) - await fetch('https://leden.djoamersfoort.nl/notifications/token', { - method: 'POST', - headers: { - authorization: `Bearer ${await user.token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - token: pushToken - }) - }) + if (Platform.OS === "android") { + await Notifications.setNotificationChannelAsync("default", { + name: "default", + importance: Notifications.AndroidImportance.DEFAULT, + vibrationPattern: [0, 250, 250, 250], + lightColor: "#FF231F7C", + }); + } + + if (!Device.isDevice) return; + + const { status: existingStatus } = await Notifications.getPermissionsAsync(); + let finalStatus = existingStatus; + if (existingStatus !== "granted") { + const { status } = await Notifications.requestPermissionsAsync(); + finalStatus = status; + } + if (finalStatus !== "granted") { + return; + } + + const config = Constants.expoConfig; + if (!config) return; + + const { data: pushToken } = await Notifications.getExpoPushTokenAsync({ + projectId: config.extra?.eas.projectId, + }); + await fetch("https://leden.djoamersfoort.nl/notifications/token", { + method: "POST", + headers: { + authorization: `Bearer ${await user.token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + token: pushToken, + }), + }); } -function createAuthState(setState: (state: AuthState) => void,discovery: DiscoveryDocument, token: string, refresh: string, expiry: number): AuthState { - const state: AuthenticatedState = { - authenticated: Authed.AUTHENTICATED, - user: jwtDecode(token), - async logout() { - await SecureStore.deleteItemAsync('id_token') - await SecureStore.deleteItemAsync('refresh_token') - await SecureStore.deleteItemAsync('expiration_date') - setState({ authenticated: Authed.UNAUTHENTICATED }) - }, - get token() { - return new Promise(async (resolve) => { - if (expiry > Date.now()) return resolve(token) - - const tokens: TokenResponse = await fetch(discovery?.tokenEndpoint!, { - method: 'post', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - grant_type: 'refresh_token', - client_id: 'QI0CNwnSLMJQbsZindMceAhtPR7lQlis0lTcCxGZ', - refresh_token: refresh - }).toString() - }).then(res => res.json()) - - token = tokens.id_token - expiry = Date.now() + tokens.expires_in * 1000 - - await SecureStore.setItemAsync('id_token', token) - await SecureStore.setItemAsync('expiration_date', expiry.toString()) - if (tokens.refresh_token) { - refresh = tokens.refresh_token - await SecureStore.setItemAsync('refresh_token', refresh) - } - - resolve(token) - }) +function createAuthState( + setState: (state: AuthState) => void, + discovery: DiscoveryDocument, + token: string, + refresh: string, + expiry: number, +): AuthState { + const state: AuthenticatedState = { + authenticated: Authed.AUTHENTICATED, + user: jwtDecode(token), + async logout() { + await SecureStore.deleteItemAsync("id_token"); + await SecureStore.deleteItemAsync("refresh_token"); + await SecureStore.deleteItemAsync("expiration_date"); + setState({ authenticated: Authed.UNAUTHENTICATED }); + }, + get token() { + return new Promise(async (resolve) => { + if (expiry > Date.now()) return resolve(token); + + const tokens: TokenResponse = await fetch(discovery?.tokenEndpoint!, { + method: "post", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "refresh_token", + client_id: "QI0CNwnSLMJQbsZindMceAhtPR7lQlis0lTcCxGZ", + refresh_token: refresh, + }).toString(), + }).then((res) => res.json()); + + token = tokens.id_token; + expiry = Date.now() + tokens.expires_in * 1000; + + await SecureStore.setItemAsync("id_token", token); + await SecureStore.setItemAsync("expiration_date", expiry.toString()); + if (tokens.refresh_token) { + refresh = tokens.refresh_token; + await SecureStore.setItemAsync("refresh_token", refresh); } - } - registerForPushNotifications(state).then() - return state + resolve(token); + }); + }, + }; + + registerForPushNotifications(state).then(); + return state; } -function AuthScreen({ discovery, setAuthenticated }: { setAuthenticated: (state: AuthState) => void, discovery: DiscoveryDocument }) { - const theme = useTheme() - - const [request, result, promptAsync] = AuthSession.useAuthRequest( - { - redirectUri, - clientId: 'QI0CNwnSLMJQbsZindMceAhtPR7lQlis0lTcCxGZ', - responseType: "code", - scopes: [ - "openid", - "user/basic", - "user/names", - "user/email", - "media", - "aanmelden", - ] +function AuthScreen({ + discovery, + setAuthenticated, +}: { + setAuthenticated: (state: AuthState) => void; + discovery: DiscoveryDocument; +}) { + const theme = useTheme(); + + const [request, result, promptAsync] = AuthSession.useAuthRequest( + { + redirectUri, + clientId: "QI0CNwnSLMJQbsZindMceAhtPR7lQlis0lTcCxGZ", + responseType: "code", + scopes: [ + "openid", + "user/basic", + "user/names", + "user/email", + "media", + "aanmelden", + ], + }, + discovery, + ); + + useEffect(() => { + async function authenticateUser() { + if (!result) return; + if (result.type === "error") { + Alert.alert( + "Authentication error", + result.params.error_description || "something went wrong", + ); + return; + } + if (result.type !== "success") return; + + setAuthenticated({ authenticated: Authed.LOADING }); + const tokens: TokenResponse = await fetch(discovery?.tokenEndpoint!, { + method: "post", + headers: { + "Content-Type": "application/x-www-form-urlencoded", }, - discovery - ) - - useEffect(() => { - async function authenticateUser() { - if (!result) return - if (result.type === "error") { - Alert.alert( - 'Authentication error', - result.params.error_description || 'something went wrong' - ) - return - } - if (result.type !== 'success') return - - setAuthenticated({ authenticated: Authed.LOADING }) - const tokens: TokenResponse = await fetch(discovery?.tokenEndpoint!, { - method: 'post', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - grant_type: 'authorization_code', - client_id: 'QI0CNwnSLMJQbsZindMceAhtPR7lQlis0lTcCxGZ', - code: result.params.code, - redirect_uri: redirectUri, - code_verifier: request?.codeVerifier - } as Record).toString() - }).then(res => res.json()) - - const token = tokens.id_token - const expiry = Date.now() + tokens.expires_in * 1000 - const refresh = tokens.refresh_token - - await SecureStore.setItemAsync('id_token', token) - await SecureStore.setItemAsync('expiration_date', expiry.toString()) - await SecureStore.setItemAsync('refresh_token', refresh!) - - setAuthenticated(createAuthState(setAuthenticated, discovery, token, refresh!, expiry)) - } + body: new URLSearchParams({ + grant_type: "authorization_code", + client_id: "QI0CNwnSLMJQbsZindMceAhtPR7lQlis0lTcCxGZ", + code: result.params.code, + redirect_uri: redirectUri, + code_verifier: request?.codeVerifier, + } as Record).toString(), + }).then((res) => res.json()); + + const token = tokens.id_token; + const expiry = Date.now() + tokens.expires_in * 1000; + const refresh = tokens.refresh_token; + + await SecureStore.setItemAsync("id_token", token); + await SecureStore.setItemAsync("expiration_date", expiry.toString()); + await SecureStore.setItemAsync("refresh_token", refresh!); + + setAuthenticated( + createAuthState(setAuthenticated, discovery, token, refresh!, expiry), + ); + } - authenticateUser().then() - }, [result]) + authenticateUser().then(); + }, [result]); - async function guest() { - await SecureStore.setItemAsync('guest', 'true') + async function guest() { + await SecureStore.setItemAsync("guest", "true"); + setAuthenticated({ + authenticated: Authed.GUEST, + login: async () => { + await SecureStore.deleteItemAsync("guest"); setAuthenticated({ - authenticated: Authed.GUEST, - login: async () => { - await SecureStore.deleteItemAsync('guest') - setAuthenticated({ - authenticated: Authed.UNAUTHENTICATED - }) - } - }) - } - - return ( - - - Of ga door als gast - - ) + authenticated: Authed.UNAUTHENTICATED, + }); + }, + }); + } + + return ( + + + + Of ga door als gast + + + ); } export function AuthProvider({ children }: { children: JSX.Element }) { - const theme = useTheme() - const discovery = AuthSession.useAutoDiscovery('https://leden.djoamersfoort.nl/o') - const [authenticated, setAuthenticated] = useState({ - authenticated: Authed.LOADING, - }) - - useEffect(() => { - if (!discovery) return - async function getAuthenticated() { - if (await SecureStore.getItemAsync('guest')) { - return setAuthenticated({ - authenticated: Authed.GUEST, - login: async () => { - await SecureStore.deleteItemAsync('guest') - setAuthenticated({ - authenticated: Authed.UNAUTHENTICATED - }) - } - }) - } - - const token = await SecureStore.getItemAsync('id_token') - const refresh = await SecureStore.getItemAsync('refresh_token') - const expiry = await SecureStore.getItemAsync('expiration_date') - - if (!token || !refresh || !expiry) { - return setAuthenticated({ - authenticated: Authed.UNAUTHENTICATED, - }) - } - - setAuthenticated(createAuthState(setAuthenticated, discovery!, token, refresh, parseInt(expiry))) - } + const theme = useTheme(); + const discovery = AuthSession.useAutoDiscovery( + "https://leden.djoamersfoort.nl/o", + ); + const [authenticated, setAuthenticated] = useState({ + authenticated: Authed.LOADING, + }); + + useEffect(() => { + if (!discovery) return; + async function getAuthenticated() { + if (await SecureStore.getItemAsync("guest")) { + return setAuthenticated({ + authenticated: Authed.GUEST, + login: async () => { + await SecureStore.deleteItemAsync("guest"); + setAuthenticated({ + authenticated: Authed.UNAUTHENTICATED, + }); + }, + }); + } + + const token = await SecureStore.getItemAsync("id_token"); + const refresh = await SecureStore.getItemAsync("refresh_token"); + const expiry = await SecureStore.getItemAsync("expiration_date"); + + if (!token || !refresh || !expiry) { + return setAuthenticated({ + authenticated: Authed.UNAUTHENTICATED, + }); + } + + setAuthenticated( + createAuthState( + setAuthenticated, + discovery!, + token, + refresh, + parseInt(expiry), + ), + ); + } - getAuthenticated().then() - }, [discovery]) - - return ( - - {discovery && authenticated.authenticated === Authed.UNAUTHENTICATED && } - {discovery && authenticated.authenticated > Authed.UNAUTHENTICATED && children} - {!discovery || authenticated.authenticated === Authed.LOADING && Loading...} - - ) + getAuthenticated().then(); + }, [discovery]); + + return ( + + {discovery && authenticated.authenticated === Authed.UNAUTHENTICATED && ( + + )} + {discovery && + authenticated.authenticated > Authed.UNAUTHENTICATED && + children} + {!discovery || + (authenticated.authenticated === Authed.LOADING && ( + + Loading... + + ))} + + ); } const styles = StyleSheet.create({ - container: { - height: '100%', - flex: 1, - flexDirection: 'column', - justifyContent: 'flex-end', - }, - button: { - margin: 15, - marginBottom: 0, - borderRadius: 25, - }, - guest: { - width: '100%', - textAlign: 'center', - padding: 15 - }, - center: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - fontSize: 20 - } -}) - -export default AuthContext + container: { + height: "100%", + flex: 1, + flexDirection: "column", + justifyContent: "flex-end", + }, + button: { + margin: 15, + marginBottom: 0, + borderRadius: 25, + }, + guest: { + width: "100%", + textAlign: "center", + padding: 15, + }, + center: { + flex: 1, + justifyContent: "center", + alignItems: "center", + fontSize: 20, + }, +}); + +export default AuthContext; diff --git a/src/components/feed/item.tsx b/src/components/feed/item.tsx index d779a70..5623def 100644 --- a/src/components/feed/item.tsx +++ b/src/components/feed/item.tsx @@ -1,46 +1,55 @@ -import {ActionType, FeedItem} from "../../stores/feed"; -import {TouchableOpacity} from "react-native"; -import * as WebBrowser from 'expo-web-browser' -import {Avatar, Card, IconButton} from "react-native-paper"; -import {useNavigation} from "@react-navigation/native"; -import {NativeStackNavigationProp} from "react-native-screens/native-stack"; -import {StackParamList} from "../../../App"; +import { ActionType, FeedItem } from "../../stores/feed"; +import { TouchableOpacity } from "react-native"; +import * as WebBrowser from "expo-web-browser"; +import { Avatar, Card, IconButton } from "react-native-paper"; +import { useNavigation } from "@react-navigation/native"; +import { NativeStackNavigationProp } from "react-native-screens/native-stack"; +import { StackParamList } from "../../../App"; -type NavigationProps = NativeStackNavigationProp +type NavigationProps = NativeStackNavigationProp; export default function Item({ item }: { item: FeedItem }) { - const navigation = useNavigation() + const navigation = useNavigation(); - async function open() { - switch (item.action.type) { - case ActionType.LINK: { - await WebBrowser.openBrowserAsync(item.action.href) - break - } - case ActionType.VIEW: { - navigation.navigate('Web', { source: item.action.source, title: item.title }) - break - } - case ActionType.ITEM: { - navigation.navigate('Item', { item: item.action.item, title: item.title }) - break - } - } + async function open() { + switch (item.action.type) { + case ActionType.LINK: { + await WebBrowser.openBrowserAsync(item.action.href); + break; + } + case ActionType.VIEW: { + navigation.navigate("Web", { + source: item.action.source, + title: item.title, + }); + break; + } + case ActionType.ITEM: { + navigation.navigate("Item", { + item: item.action.item, + title: item.title, + }); + break; + } } + } - return ( - - - (item.icon.startsWith('http') ? - : - - )} - right={(props) => } - /> - - - ) + return ( + + + + item.icon.startsWith("http") ? ( + + ) : ( + + ) + } + right={(props) => } + /> + + + ); } diff --git a/src/components/media/preview.tsx b/src/components/media/preview.tsx index b496195..7278d51 100644 --- a/src/components/media/preview.tsx +++ b/src/components/media/preview.tsx @@ -1,32 +1,37 @@ -import {AlbumList} from "../../__generated__/media"; -import {Image, StyleSheet, TouchableOpacity} from "react-native"; +import { AlbumList } from "../../__generated__/media"; +import { Image, StyleSheet, TouchableOpacity } from "react-native"; import { Text } from "react-native-paper"; -import {useNavigation} from "@react-navigation/native"; -import {NativeStackNavigationProp} from "react-native-screens/native-stack"; -import {StackParamList} from "../../../App"; +import { useNavigation } from "@react-navigation/native"; +import { NativeStackNavigationProp } from "react-native-screens/native-stack"; +import { StackParamList } from "../../../App"; -type NavigationProps = NativeStackNavigationProp +type NavigationProps = NativeStackNavigationProp; export default function Preview({ album }: { album: AlbumList }) { - const navigation = useNavigation() + const navigation = useNavigation(); - return ( - navigation.navigate('Album', { album: album.id, title: album.name })}> - - {album.name} - - ) + return ( + + navigation.navigate("Album", { album: album.id, title: album.name }) + } + > + + {album.name} + + ); } const styles = StyleSheet.create({ - container: { - flex: 1 / 2, - margin: 5, - gap: 5, - }, - image: { - aspectRatio: 1, - borderRadius: 10, - backgroundColor: "lightgrey", - }, -}) + container: { + flex: 1 / 2, + margin: 5, + gap: 5, + }, + image: { + aspectRatio: 1, + borderRadius: 10, + backgroundColor: "lightgrey", + }, +}); diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index e8c01fe..5e0ed3b 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -1,15 +1,20 @@ -import { Appbar } from 'react-native-paper'; -import { getHeaderTitle } from '@react-navigation/elements'; -import {StackHeaderProps} from "@react-navigation/stack"; +import { Appbar } from "react-native-paper"; +import { getHeaderTitle } from "@react-navigation/elements"; +import { StackHeaderProps } from "@react-navigation/stack"; -export default function CustomNavigationBar({ navigation, route, options, back }: StackHeaderProps) { - const title = getHeaderTitle(options, route.name); +export default function CustomNavigationBar({ + navigation, + route, + options, + back, +}: StackHeaderProps) { + const title = getHeaderTitle(options, route.name); - return ( - - {back ? : null} - - {options.headerRight && } - - ) + return ( + + {back ? : null} + + {options.headerRight && } + + ); } diff --git a/src/screens/feed/item.tsx b/src/screens/feed/item.tsx index 5ee7267..d8fac54 100644 --- a/src/screens/feed/item.tsx +++ b/src/screens/feed/item.tsx @@ -1,28 +1,32 @@ -import {StyleSheet, View} from "react-native"; -import {StackScreenProps} from "@react-navigation/stack"; -import {StackParamList} from "../../../App"; -import {Card, Text} from "react-native-paper"; +import { StyleSheet, View } from "react-native"; +import { StackScreenProps } from "@react-navigation/stack"; +import { StackParamList } from "../../../App"; +import { Card, Text } from "react-native-paper"; -type Props = StackScreenProps +type Props = StackScreenProps; export default function ItemScreen({ route }: Props) { - const { item } = route.params + const { item } = route.params; - return ( - - - - - - {item.location_description} - - - - ) + return ( + + + + + + {item.location_description} + + + + ); } const styles = StyleSheet.create({ - container: { - padding: 10, - } -}) + container: { + padding: 10, + }, +}); diff --git a/src/screens/feed/search.tsx b/src/screens/feed/search.tsx index cc1240d..df1c3f3 100644 --- a/src/screens/feed/search.tsx +++ b/src/screens/feed/search.tsx @@ -1,146 +1,156 @@ -import {Card, Searchbar, useTheme} from "react-native-paper"; -import {useState} from "react"; -import {SafeAreaView, ScrollView, StyleSheet, View} from "react-native"; -import {useNavigation} from "@react-navigation/native"; -import {ActionType, FeedItem} from "../../stores/feed"; +import { Card, Searchbar, useTheme } from "react-native-paper"; +import { useState } from "react"; +import { SafeAreaView, ScrollView, StyleSheet, View } from "react-native"; +import { useNavigation } from "@react-navigation/native"; +import { ActionType, FeedItem } from "../../stores/feed"; import Item from "../../components/feed/item"; -import { parse } from 'fast-html-parser' +import { parse } from "fast-html-parser"; export interface Item { - id: number, - name: string, - description: string, - location: string, - location_description: string, - location_id: 7, - url: string, - properties: string[] + id: number; + name: string; + description: string; + location: string; + location_description: string; + location_id: 7; + url: string; + properties: string[]; } interface Article { - id: string, - title: string, - url: string, - _embedded: { - self: [{ - excerpt: { - rendered: string - } - }] - } + id: string; + title: string; + url: string; + _embedded: { + self: [ + { + excerpt: { + rendered: string; + }; + }, + ]; + }; } async function getItems(query: string) { - const { items }: { items: Item[] } = await fetch(`https://inventory.djoamersfoort.nl/api/v1/items/search/${encodeURI(query)}`) - .then(res => res.json()) + const { items }: { items: Item[] } = await fetch( + `https://inventory.djoamersfoort.nl/api/v1/items/search/${encodeURI(query)}`, + ).then((res) => res.json()); - return items.map(item => ({ + return items.map( + (item) => + ({ title: item.name, description: item.location_description, - icon: 'package-variant-closed', + icon: "package-variant-closed", action: { - type: ActionType.ITEM, - item - } - } as FeedItem)) + type: ActionType.ITEM, + item, + }, + }) as FeedItem, + ); } async function getArticles(query: string) { - const articles: Article[] = await fetch(`https://djoamersfoort.nl/wp-json/wp/v2/search?_embed&search=${encodeURI(query)}`) - .then(res => res.json()) + const articles: Article[] = await fetch( + `https://djoamersfoort.nl/wp-json/wp/v2/search?_embed&search=${encodeURI(query)}`, + ).then((res) => res.json()); - return articles.map(article => { - const excerpt = parse(article._embedded.self[0].excerpt.rendered) + return articles.map((article) => { + const excerpt = parse(article._embedded.self[0].excerpt.rendered); - const result: FeedItem = { - title: article.title, - description: excerpt.querySelector('p')?.text || '', - icon: 'post', - action: { - type: ActionType.LINK, - href: article.url - } - } + const result: FeedItem = { + title: article.title, + description: excerpt.querySelector("p")?.text || "", + icon: "post", + action: { + type: ActionType.LINK, + href: article.url, + }, + }; - return result - }) + return result; + }); } export default function SearchScreen() { - const theme = useTheme() - const [search, setSearch] = useState('') - const navigation = useNavigation() + const theme = useTheme(); + const [search, setSearch] = useState(""); + const navigation = useNavigation(); - const [itemResults, setItemsResults] = useState([]) - const [articleResults, setArticleResults] = useState([]) + const [itemResults, setItemsResults] = useState([]); + const [articleResults, setArticleResults] = useState([]); - async function updateResults() { - if (!search) return - getItems(search).then(setItemsResults) - getArticles(search).then(setArticleResults) - } + async function updateResults() { + if (!search) return; + getItems(search).then(setItemsResults); + getArticles(search).then(setArticleResults); + } - return ( - - - - - - - {itemResults.length > 0 && ( - - - - {itemResults.map((result, index) => ( - - ))} - - - )} - {articleResults.length > 0 && ( - - - - {articleResults.map((result, index) => ( - - ))} - - - )} - - - - - ) + return ( + + + + + + + {itemResults.length > 0 && ( + + + + {itemResults.map((result, index) => ( + + ))} + + + )} + {articleResults.length > 0 && ( + + + + {articleResults.map((result, index) => ( + + ))} + + + )} + + + + ); } const styles = StyleSheet.create({ - view: { - flex: 1, - flexDirection: "column", - height: '100%' - }, - component: { - padding: 10 - }, - results: { - paddingBottom: 10, - gap: 10 - }, - content: { - gap: 10 - } -}) + view: { + flex: 1, + flexDirection: "column", + height: "100%", + }, + component: { + padding: 10, + }, + results: { + paddingBottom: 10, + gap: 10, + }, + content: { + gap: 10, + }, +}); diff --git a/src/screens/media/album.tsx b/src/screens/media/album.tsx index d09bd7e..c1cf5ec 100644 --- a/src/screens/media/album.tsx +++ b/src/screens/media/album.tsx @@ -1,126 +1,147 @@ -import {FlatList, StyleSheet, TouchableOpacity, Image, Alert} from "react-native"; -import {useContext, useEffect, useState} from "react"; -import {Album} from "../../__generated__/media"; -import {useAtom} from "jotai"; -import {apiAtom} from "../../stores/media"; -import {StackScreenProps} from "@react-navigation/stack"; -import {StackParamList} from "../../../App"; -import {Appbar, Text} from 'react-native-paper' -import {useNavigation} from "@react-navigation/native"; -import {NativeStackNavigationProp} from "react-native-screens/native-stack"; -import {NativeStackNavigationEventMap} from "react-native-screens/lib/typescript/native-stack/types"; -import * as ImagePicker from 'expo-image-picker' -import AuthContext, {Authed} from "../../auth"; - -type Props = StackScreenProps -type NavigationProps = NativeStackNavigationProp +import { + FlatList, + StyleSheet, + TouchableOpacity, + Image, + Alert, +} from "react-native"; +import { useContext, useEffect, useState } from "react"; +import { Album } from "../../__generated__/media"; +import { useAtom } from "jotai"; +import { apiAtom } from "../../stores/media"; +import { StackScreenProps } from "@react-navigation/stack"; +import { StackParamList } from "../../../App"; +import { Appbar, Text } from "react-native-paper"; +import { useNavigation } from "@react-navigation/native"; +import { NativeStackNavigationProp } from "react-native-screens/native-stack"; +import { NativeStackNavigationEventMap } from "react-native-screens/lib/typescript/native-stack/types"; +import * as ImagePicker from "expo-image-picker"; +import AuthContext, { Authed } from "../../auth"; + +type Props = StackScreenProps; +type NavigationProps = NativeStackNavigationProp; export default function AlbumScreen({ route }: Props) { - const navigation = useNavigation() - const authState = useContext(AuthContext) - const [api] = useAtom(apiAtom) - const [album, setAlbum] = useState() - - const [_cameraStatus, requestPermissions, getPermissions] = ImagePicker.useCameraPermissions(); - - async function upload(images: ImagePicker.ImagePickerAsset[]) { - if (authState.authenticated !== Authed.AUTHENTICATED || !api) return - - const formData = new FormData() - images.forEach(asset => { - // @ts-ignore - formData.append('items', { uri: asset.uri, name: 'file', type: asset.mimeType }) - }) - - await fetch(`https://media.djoamersfoort.nl/api/items/${route.params.album}`, { - method: 'POST', - headers: { - authorization: `Bearer ${await authState.token}` - }, - body: formData, - }) - - const { data } = await api.albums.getAlbum(route.params.album) - setAlbum(data) + const navigation = useNavigation(); + const authState = useContext(AuthContext); + const [api] = useAtom(apiAtom); + const [album, setAlbum] = useState(); + + const [_cameraStatus, requestPermissions, getPermissions] = + ImagePicker.useCameraPermissions(); + + async function upload(images: ImagePicker.ImagePickerAsset[]) { + if (authState.authenticated !== Authed.AUTHENTICATED || !api) return; + + const formData = new FormData(); + images.forEach((asset) => { + // @ts-ignore + formData.append("items", { + uri: asset.uri, + name: "file", + type: asset.mimeType, + }); + }); + + await fetch( + `https://media.djoamersfoort.nl/api/items/${route.params.album}`, + { + method: "POST", + headers: { + authorization: `Bearer ${await authState.token}`, + }, + body: formData, + }, + ); + + const { data } = await api.albums.getAlbum(route.params.album); + setAlbum(data); + } + + async function selectImages() { + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.All, + allowsMultipleSelection: true, + }); + if (result.canceled) return; + + await upload(result.assets); + } + + async function captureImages() { + const permissions = await getPermissions(); + if (!permissions.granted) { + if (!permissions.canAskAgain) + return Alert.alert( + "Camera permissions denied!", + "Change permissions in your device's settings", + ); + + const result = await requestPermissions(); + if (!result.granted) + return Alert.alert( + "Camera permissions denied!", + "Change permissions in your device's settings", + ); } - async function selectImages() { - const result = await ImagePicker.launchImageLibraryAsync({ - mediaTypes: ImagePicker.MediaTypeOptions.All, - allowsMultipleSelection: true - }) - if (result.canceled) return - - await upload(result.assets) + const result = await ImagePicker.launchCameraAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.All, + allowsMultipleSelection: true, + }); + if (result.canceled) return; + + await upload(result.assets); + } + + useEffect(() => { + async function fetchAlbum() { + if (!api) return; + + navigation.setOptions({ + headerRight: () => ( + <> + + + + ), + } as Partial); + const { data: album } = await api.albums.getAlbum(route.params.album); + setAlbum(album); } - async function captureImages() { - const permissions = await getPermissions() - if (!permissions.granted) { - if (!permissions.canAskAgain) return Alert.alert('Camera permissions denied!', 'Change permissions in your device\'s settings') - - const result = await requestPermissions() - if (!result.granted) return Alert.alert('Camera permissions denied!', 'Change permissions in your device\'s settings') - } - - const result = await ImagePicker.launchCameraAsync({ - mediaTypes: ImagePicker.MediaTypeOptions.All, - allowsMultipleSelection: true - }) - if (result.canceled) return - - await upload(result.assets) - } - - useEffect(() => { - async function fetchAlbum() { - if (!api) return - - navigation.setOptions({ - headerRight: () => ( - <> - - - - ) - } as Partial) - const { data: album } = await api.albums.getAlbum(route.params.album) - setAlbum(album) - } - - fetchAlbum().then() - }, [api]) - - function openImage(image: number) { - if (!album) return - - navigation.navigate('Slides', { items: album.items, item: image }) - } - - - if (!album) return Loading... - return ( - item.id} - renderItem={({ item, index }) => ( - openImage(index)}> - - - )} - /> - ) + fetchAlbum().then(); + }, [api]); + + function openImage(image: number) { + if (!album) return; + + navigation.navigate("Slides", { items: album.items, item: image }); + } + + if (!album) return Loading...; + return ( + item.id} + renderItem={({ item, index }) => ( + openImage(index)}> + + + )} + /> + ); } const styles = StyleSheet.create({ - item: { - flex: 1 / 3, - aspectRatio: 1, - }, - image: { - flex: 1, - resizeMode: "cover", - }, -}) + item: { + flex: 1 / 3, + aspectRatio: 1, + }, + image: { + flex: 1, + resizeMode: "cover", + }, +}); diff --git a/src/screens/media/media.tsx b/src/screens/media/media.tsx index 4b9b40f..63a6354 100644 --- a/src/screens/media/media.tsx +++ b/src/screens/media/media.tsx @@ -1,68 +1,70 @@ -import {useContext, useEffect, useState} from "react"; -import AuthContext, {Authed} from "../../auth"; -import {useAtom} from "jotai"; -import {apiAtom, getApi} from "../../stores/media"; -import {AlbumList} from "../../__generated__/media"; -import {FlatList, StyleSheet, View} from "react-native"; +import { useContext, useEffect, useState } from "react"; +import AuthContext, { Authed } from "../../auth"; +import { useAtom } from "jotai"; +import { apiAtom, getApi } from "../../stores/media"; +import { AlbumList } from "../../__generated__/media"; +import { FlatList, StyleSheet, View } from "react-native"; import Preview from "../../components/media/preview"; -import {Appbar, Button, Text} from "react-native-paper"; +import { Appbar, Button, Text } from "react-native-paper"; export default function MediaScreen() { - const authState = useContext(AuthContext) - const [api, setApi] = useAtom(apiAtom) - const [albums, setAlbums] = useState([]) + const authState = useContext(AuthContext); + const [api, setApi] = useAtom(apiAtom); + const [albums, setAlbums] = useState([]); - useEffect(() => { - if (authState.authenticated !== Authed.AUTHENTICATED) { - return setApi(null) - } + useEffect(() => { + if (authState.authenticated !== Authed.AUTHENTICATED) { + return setApi(null); + } - authState.token.then(token => { - setApi(getApi(token)) - }) - }, [authState]) - useEffect(() => { - async function getAlbums() { - if (!api) return + authState.token.then((token) => { + setApi(getApi(token)); + }); + }, [authState]); + useEffect(() => { + async function getAlbums() { + if (!api) return; - const { data: albums } = await api.albums.getAlbums() - setAlbums(albums.sort((a, b) => a.order - b.order)) - } + const { data: albums } = await api.albums.getAlbums(); + setAlbums(albums.sort((a, b) => a.order - b.order)); + } - getAlbums().then() - }, [api]) + getAlbums().then(); + }, [api]); - return ( - <> - - - - {authState.authenticated === Authed.GUEST && ( - - Je moet eerst inloggen om deze pagina te bekijken - - - )} - {authState.authenticated === Authed.AUTHENTICATED && ( - item.id} - renderItem={({ item }) => } - /> - )} - - ) + return ( + <> + + + + {authState.authenticated === Authed.GUEST && ( + + Je moet eerst inloggen om deze pagina te bekijken + + + )} + {authState.authenticated === Authed.AUTHENTICATED && ( + item.id} + renderItem={({ item }) => } + /> + )} + + ); } const styles = StyleSheet.create({ - unauthenticated: { - flex: 1, - flexDirection: "column", - justifyContent: "flex-end", - gap: 5, - padding: 20, - textAlign: "center" - } -}) + unauthenticated: { + flex: 1, + flexDirection: "column", + justifyContent: "flex-end", + gap: 5, + padding: 20, + textAlign: "center", + }, +}); diff --git a/src/screens/media/slides.tsx b/src/screens/media/slides.tsx index 2f065e0..d36e252 100644 --- a/src/screens/media/slides.tsx +++ b/src/screens/media/slides.tsx @@ -1,65 +1,69 @@ -import {StackScreenProps} from "@react-navigation/stack"; -import {StackParamList} from "../../../App"; -import {Image, StyleSheet, View} from "react-native"; +import { StackScreenProps } from "@react-navigation/stack"; +import { StackParamList } from "../../../App"; +import { Image, StyleSheet, View } from "react-native"; import PagerView from "react-native-pager-view"; -import {useEffect, useState} from "react"; -import {useNavigation} from "@react-navigation/native"; -import {ResizeMode, Video} from "expo-av"; +import { useEffect, useState } from "react"; +import { useNavigation } from "@react-navigation/native"; +import { ResizeMode, Video } from "expo-av"; -type Props = StackScreenProps +type Props = StackScreenProps; export default function SlidesScreen({ route }: Props) { - const { items, item } = route.params - const [page, setPage] = useState(item) - const navigation = useNavigation() + const { items, item } = route.params; + const [page, setPage] = useState(item); + const navigation = useNavigation(); - useEffect(() => { - const title = new Date(items[page].date) - .toLocaleDateString('nl-NL', { - year: 'numeric', - month: 'long', - day: 'numeric', - }) + useEffect(() => { + const title = new Date(items[page].date).toLocaleDateString("nl-NL", { + year: "numeric", + month: "long", + day: "numeric", + }); - navigation.setOptions({ - title - }) - }, [page]) + navigation.setOptions({ + title, + }); + }, [page]); - function inRange(x: number, y: number, range: number) { - return x >= y - range && x <= y + range - } + function inRange(x: number, y: number, range: number) { + return x >= y - range && x <= y + range; + } - return ( - - setPage(event.nativeEvent.position)} - > - {items.map((item, index) => ( - - {inRange(page, index, 1) && ( - (item.type === 1 ? - : - - ))} - - - ) + return ( + + setPage(event.nativeEvent.position)} + > + {items.map((item, index) => ( + + {inRange(page, index, 1) && + (item.type === 1 ? ( + + ) : ( + + ))} + + + ); } const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: "center", - }, - image: { - width: "100%", - height: "100%", - resizeMode: "contain", - }, -}) + container: { + flex: 1, + justifyContent: "center", + }, + image: { + width: "100%", + height: "100%", + resizeMode: "contain", + }, +}); diff --git a/src/screens/settings.tsx b/src/screens/settings.tsx index bb3950d..b24fb5d 100644 --- a/src/screens/settings.tsx +++ b/src/screens/settings.tsx @@ -1,48 +1,55 @@ -import {useContext} from "react"; -import AuthContext, {Authed} from "../auth"; -import {ScrollView, StyleSheet, View} from "react-native"; -import {Appbar, Button, Card, Text} from "react-native-paper"; +import { useContext } from "react"; +import AuthContext, { Authed } from "../auth"; +import { ScrollView, StyleSheet, View } from "react-native"; +import { Appbar, Button, Card, Text } from "react-native-paper"; import auth from "../auth"; export default function SettingsScreen() { - const authState = useContext(AuthContext) + const authState = useContext(AuthContext); - return ( - <> - - - - - - - - {authState.authenticated === Authed.AUTHENTICATED && ( - - {authState.user.given_name} {authState.user.family_name} ({authState.user.sub}) - - - )} - {authState.authenticated === Authed.GUEST && ( - - Niet ingelogd - - - )} - - - - - ) + return ( + <> + + + + + + + + {authState.authenticated === Authed.AUTHENTICATED && ( + + + {authState.user.given_name} {authState.user.family_name} ( + {authState.user.sub}) + + + + )} + {authState.authenticated === Authed.GUEST && ( + + Niet ingelogd + + + )} + + + + + ); } const styles = StyleSheet.create({ - container: { - padding: 10, - gap: 10 - }, - content: { - gap: 5, - padding: 17, - paddingTop: 0 - } -}) + container: { + padding: 10, + gap: 10, + }, + content: { + gap: 5, + padding: 17, + paddingTop: 0, + }, +}); diff --git a/src/screens/web.tsx b/src/screens/web.tsx index b647ac3..4f7729e 100644 --- a/src/screens/web.tsx +++ b/src/screens/web.tsx @@ -1,33 +1,33 @@ import { WebView } from "react-native-webview"; -import {StackScreenProps} from "@react-navigation/stack"; -import {StackParamList} from "../../App"; -import {MD3Theme, useTheme} from "react-native-paper"; -import {useEffect, useState} from "react"; +import { StackScreenProps } from "@react-navigation/stack"; +import { StackParamList } from "../../App"; +import { MD3Theme, useTheme } from "react-native-paper"; +import { useEffect, useState } from "react"; -type Props = StackScreenProps +type Props = StackScreenProps; function getCSS(theme: MD3Theme) { - return "body {\n" + - ` background-color: ${theme.colors.background};\n` + - ` color: ${theme.colors.onBackground};\n` + - " font-family: sans-serif;\n" + - " font-size: 20px !important;\n" + - " margin: 20px;\n" + - " }\n" + - "* {\n" + - " font-size: 1.5em !important;\n" + - "}" + return ( + "body {\n" + + ` background-color: ${theme.colors.background};\n` + + ` color: ${theme.colors.onBackground};\n` + + " font-family: sans-serif;\n" + + " font-size: 20px !important;\n" + + " margin: 20px;\n" + + " }\n" + + "* {\n" + + " font-size: 1.5em !important;\n" + + "}" + ); } export default function WebScreen({ route }: Props) { - const theme = useTheme() - const [source, setSource] = useState('') + const theme = useTheme(); + const [source, setSource] = useState(""); - useEffect(() => { - setSource(`${route.params.source}`) - }, [theme]) + useEffect(() => { + setSource(`${route.params.source}`); + }, [theme]); - return ( - - ) + return ; } diff --git a/src/stores/media.ts b/src/stores/media.ts index f7d7eea..425d083 100644 --- a/src/stores/media.ts +++ b/src/stores/media.ts @@ -1,17 +1,17 @@ -import {atom} from "jotai"; -import {Api} from "../__generated__/media"; +import { atom } from "jotai"; +import { Api } from "../__generated__/media"; -export const apiAtom = atom|null>(null) +export const apiAtom = atom | null>(null); export function getApi(token: string) { - return new Api({ - baseUrl: 'https://media.djoamersfoort.nl/api', - baseApiParams: { - credentials: 'same-origin', - headers: { - authorization: `Bearer ${token}` - }, - redirect: 'follow', - referrerPolicy: 'no-referrer', - } - }) + return new Api({ + baseUrl: "https://media.djoamersfoort.nl/api", + baseApiParams: { + credentials: "same-origin", + headers: { + authorization: `Bearer ${token}`, + }, + redirect: "follow", + referrerPolicy: "no-referrer", + }, + }); } diff --git a/yarn.lock b/yarn.lock index 942803f..1aad01f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5865,6 +5865,11 @@ prepend-http@^2.0.0: resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz" integrity sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA== +prettier@^3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" + integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== + pretty-bytes@5.6.0: version "5.6.0" resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz" From 4ad823326a4bce7f2fae22e3fde46d396cdbc3aa Mon Sep 17 00:00:00 2001 From: Sverre <59171289+sverben@users.noreply.github.com> Date: Fri, 26 Apr 2024 19:51:16 +0200 Subject: [PATCH 2/2] chore: beta release 1.2 --- app.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app.json b/app.json index e5ab2f2..9f1db79 100644 --- a/app.json +++ b/app.json @@ -3,7 +3,7 @@ "name": "djo", "slug": "djo", "scheme": "djo", - "version": "1.1.0", + "version": "1.2.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "automatic", @@ -16,7 +16,7 @@ "**/*" ], "ios": { - "buildNumber": "2", + "buildNumber": "1", "userInterfaceStyle": "automatic", "supportsTablet": false, "bundleIdentifier": "nl.djoamersfoort.djo", @@ -39,7 +39,7 @@ "backgroundColor": "#202088" }, "googleServicesFile": "./google-services.json", - "versionCode": 3 + "versionCode": 4 }, "web": { "favicon": "./assets/favicon.png"