Skip to content
This repository was archived by the owner on Apr 19, 2023. It is now read-only.

Commit 0f13aa5

Browse files
💥 Add v3 routes, components
1 parent 48e37f3 commit 0f13aa5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+4713
-1
lines changed

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/ambient.d.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* These declarations tell TypeScript that we allow import of images, e.g.
3+
* ```
4+
<script lang='ts'>
5+
import successkid from 'images/successkid.jpg';
6+
</script>
7+
8+
<img src="{successkid}">
9+
```
10+
*/
11+
declare module "*.gif" {
12+
const value: string;
13+
export = value;
14+
}
15+
16+
declare module "*.jpg" {
17+
const value: string;
18+
export = value;
19+
}
20+
21+
declare module "*.jpeg" {
22+
const value: string;
23+
export = value;
24+
}
25+
26+
declare module "*.png" {
27+
const value: string;
28+
export = value;
29+
}
30+
31+
declare module "*.svg" {
32+
const value: string;
33+
export = value;
34+
}
35+
36+
declare module "*.webp" {
37+
const value: string;
38+
export = value;
39+
}

src/api.ts

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { users, activeUserIndex } from "./stores";
2+
import { errors } from "./errors";
3+
import type { User } from "./stores";
4+
import decode from "jwt-decode";
5+
import wcmatch from "wildcard-match";
6+
import { titleCase } from "./helpers/string-utils";
7+
import { getItem, setItem, removeItem } from "localforage";
8+
9+
const BASE_URL =
10+
process.env.NODE_ENV === "development" ? "http://localhost:3001/v1" : "http://localhost:3001/v1";
11+
12+
let loggedInUsers: User[] = [];
13+
users.subscribe((users) => (loggedInUsers = users));
14+
15+
let index: number = 0;
16+
activeUserIndex.subscribe((i) => (index = i));
17+
18+
let refreshing = false;
19+
20+
export const can = (scope: string): boolean => {
21+
try {
22+
const user = loggedInUsers[index];
23+
const token = user.auth.accessToken;
24+
const { scopes } = decode<{ scopes: string[] }>(token);
25+
const isMatch = wcmatch(scopes);
26+
return isMatch(scope);
27+
} catch (error) {}
28+
return false;
29+
};
30+
31+
export const api = async <T>({
32+
method = "GET",
33+
url,
34+
onCachedResponse,
35+
body,
36+
token = "",
37+
formData = false,
38+
blob = false,
39+
}: {
40+
method?: "GET" | "POST" | "PATCH" | "DELETE";
41+
url: string;
42+
onCachedResponse?: (data: T) => any;
43+
body?: any;
44+
token?: string;
45+
formData?: boolean;
46+
blob?: boolean;
47+
}): Promise<T> => {
48+
if (refreshing) await wait(5000);
49+
const cacheKey = `api-cache-${url}`;
50+
51+
try {
52+
if (method === "GET" && typeof onCachedResponse === "function") {
53+
const item = await getItem<{ data: T; date: Date }>(cacheKey);
54+
if (item) {
55+
if (new Date().getTime() - item.date.getTime() > 86400000) removeItem(cacheKey);
56+
else onCachedResponse(item.data);
57+
}
58+
}
59+
} catch (error) {}
60+
61+
if (loggedInUsers.length && !token) {
62+
const user = loggedInUsers[index];
63+
if (user) token = user.auth.accessToken;
64+
}
65+
66+
if (loggedInUsers.length && loggedInUsers[index]) {
67+
let isExpiredJwt = false;
68+
try {
69+
const { exp } = decode<{ exp: number }>(token);
70+
isExpiredJwt = new Date().getTime() > new Date(exp * 1000).getTime();
71+
} catch (error) {}
72+
if (isExpiredJwt) {
73+
try {
74+
const res = await fetch(`${BASE_URL}/auth/refresh`, {
75+
method: "POST",
76+
body: JSON.stringify({
77+
token: loggedInUsers[index].auth.refreshToken,
78+
}),
79+
headers: {
80+
"X-Requested-With": "XmlHttpRequest",
81+
Accept: "application/json",
82+
"Content-Type": "application/json",
83+
},
84+
});
85+
const {
86+
accessToken,
87+
refreshToken,
88+
}: { accessToken: string; refreshToken: string } = await res.json();
89+
users.update((val) =>
90+
val.map((user, i) => {
91+
if (index === i) {
92+
return {
93+
details: user.details,
94+
auth: { accessToken, refreshToken },
95+
};
96+
}
97+
return user;
98+
})
99+
);
100+
token = accessToken;
101+
} catch (error) {}
102+
}
103+
}
104+
105+
const options: RequestInit = {
106+
method,
107+
headers: {
108+
"X-Requested-With": "XmlHttpRequest",
109+
Accept: "application/json",
110+
Authorization: token && `Bearer ${token}`,
111+
},
112+
};
113+
if (!formData) options.headers["Content-Type"] = "application/json";
114+
if (body && !formData) options.body = JSON.stringify(body);
115+
else options.body = body;
116+
117+
const res = await fetch(`${BASE_URL}${url}`, options);
118+
if (!res.ok) {
119+
let errorTitle = "";
120+
if (res.status === 404) errorTitle = "Not found";
121+
if (res.status === 400) errorTitle = "Oops, something went wrong";
122+
if (res.status === 429) errorTitle = "Slow down, tiger!";
123+
124+
if (res.status === 401) {
125+
users.set([]);
126+
activeUserIndex.set(0);
127+
window.location.href = "/";
128+
}
129+
130+
let errorMessage = "An unknown error occurred in performing your request";
131+
try {
132+
const json = await res.json();
133+
if (json.message) {
134+
if (Array.isArray(json.message)) errorMessage = json.message.map(titleCase).join(", ");
135+
else errorMessage = errors[json.message.split(":")[0]] || String(json.message);
136+
}
137+
} catch (error) {}
138+
139+
throw new Error(errorTitle ? `${errorMessage}\n${errorTitle}` : errorMessage);
140+
}
141+
if (blob) {
142+
const blob = await res.blob();
143+
return (blob as any) as T;
144+
} else {
145+
const json: T = await res.json();
146+
if (method === "GET" && !url.includes("cursor"))
147+
await setItem(cacheKey, { date: new Date(), data: json });
148+
return json;
149+
}
150+
};
151+
152+
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
153+
154+
export const loginWithTokenResponse = async (auth: User["auth"]) => {
155+
if (!auth.accessToken) return;
156+
const userId = decode<{ id: number }>(auth.accessToken).id;
157+
const details = await api<User["details"]>({
158+
method: "GET",
159+
url: `/users/${userId}`,
160+
token: auth.accessToken,
161+
});
162+
users.update((val) =>
163+
[...val, { details, auth }].filter(
164+
(v, i, a) => a.map((i) => i.details.id).indexOf(v.details.id) === i
165+
)
166+
);
167+
activeUserIndex.set(loggedInUsers.length - 1);
168+
};

src/client.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as sapper from "@sapper/app";
2+
3+
sapper.start({
4+
target: document.querySelector("#sapper"),
5+
});

0 commit comments

Comments
 (0)