+86
app/[doc_id]/Blocks.tsx
+86
app/[doc_id]/Blocks.tsx
···
1
1
"use client";
2
2
import { useEntity, useReplicache } from "../../replicache";
3
+
import NextImage from "next/image";
3
4
import { TextBlock } from "../../components/TextBlock";
4
5
import { generateKeyBetween } from "fractional-indexing";
6
+
import { supabaseBrowserClient } from "../../supabase/browserClient";
5
7
export function AddBlock(props: { entityID: string }) {
6
8
let rep = useReplicache();
7
9
let blocks = useEntity(props.entityID, "card/block")?.sort((a, b) => {
···
22
24
);
23
25
}
24
26
27
+
export function AddImageBlock(props: { entityID: string }) {
28
+
let rep = useReplicache();
29
+
let blocks = useEntity(props.entityID, "card/block")?.sort((a, b) => {
30
+
return a.data.position > b.data.position ? 1 : -1;
31
+
});
32
+
return (
33
+
<input
34
+
type="file"
35
+
accept="image/*"
36
+
onChange={async (e) => {
37
+
let file = e.currentTarget.files?.[0];
38
+
if (!file) return;
39
+
let client = supabaseBrowserClient();
40
+
let cache = await caches.open("minilink-user-assets");
41
+
let hash = await computeHash(file);
42
+
let url = client.storage.from("minilink-user-assets").getPublicUrl(hash)
43
+
.data.publicUrl;
44
+
let dimensions = await getImageDimensions(file);
45
+
await cache.put(
46
+
url,
47
+
new Response(file, {
48
+
headers: {
49
+
"Content-Type": file.type,
50
+
"Content-Length": file.size.toString(),
51
+
},
52
+
}),
53
+
);
54
+
let newBlockEntity = crypto.randomUUID();
55
+
await rep?.rep?.mutate.addBlock({
56
+
parent: props.entityID,
57
+
position: generateKeyBetween(null, blocks[0]?.data.position || null),
58
+
newEntityID: newBlockEntity,
59
+
});
60
+
await rep?.rep?.mutate.assertFact({
61
+
entity: newBlockEntity,
62
+
attribute: "block/image",
63
+
data: {
64
+
type: "image",
65
+
src: url,
66
+
height: dimensions.height,
67
+
width: dimensions.width,
68
+
},
69
+
});
70
+
await client.storage.from("minilink-user-assets").upload(hash, file);
71
+
}}
72
+
/>
73
+
);
74
+
}
75
+
76
+
function getImageDimensions(
77
+
file: File,
78
+
): Promise<{ width: number; height: number }> {
79
+
let url = URL.createObjectURL(file);
80
+
return new Promise((resolve, reject) => {
81
+
const img = new Image();
82
+
img.onload = function () {
83
+
resolve({ width: img.width, height: img.height });
84
+
URL.revokeObjectURL(url);
85
+
};
86
+
img.onerror = reject;
87
+
img.src = url;
88
+
});
89
+
}
90
+
91
+
async function computeHash(data: File): Promise<string> {
92
+
let buffer = await data.arrayBuffer();
93
+
const buf = await crypto.subtle.digest("SHA-256", new Uint8Array(buffer));
94
+
return Array.from(new Uint8Array(buf), (b) =>
95
+
b.toString(16).padStart(2, "0"),
96
+
).join("");
97
+
}
98
+
25
99
export function Blocks(props: { entityID: string }) {
26
100
let blocks = useEntity(props.entityID, "card/block");
27
101
···
54
128
previousBlock: { position: string; value: string } | null;
55
129
nextPosition: string | null;
56
130
}) {
131
+
let image = useEntity(props.entityID, "block/image");
132
+
if (image)
133
+
return (
134
+
<div className="border p-2 w-full">
135
+
<img
136
+
alt={""}
137
+
src={image.data.src}
138
+
height={image.data.height}
139
+
width={image.data.width}
140
+
/>
141
+
</div>
142
+
);
57
143
return (
58
144
<div className="border p-2 w-full">
59
145
<TextBlock {...props} />
+5
-3
app/[doc_id]/page.tsx
+5
-3
app/[doc_id]/page.tsx
···
1
-
import { createClient } from "@supabase/supabase-js";
2
1
import { Fact, ReplicacheProvider } from "../../replicache";
3
2
import { Database } from "../../supabase/database.types";
4
-
import { AddBlock, Blocks } from "./Blocks";
3
+
import { AddBlock, AddImageBlock, Blocks } from "./Blocks";
5
4
import { Attributes } from "../../replicache/attributes";
5
+
import { createServerClient } from "@supabase/ssr";
6
6
7
7
export const preferredRegion = ["sfo1"];
8
8
export const dynamic = "force-dynamic";
9
9
10
-
let supabase = createClient<Database>(
10
+
let supabase = createServerClient<Database>(
11
11
process.env.NEXT_PUBLIC_SUPABASE_API_URL as string,
12
12
process.env.SUPABASE_SERVICE_ROLE_KEY as string,
13
+
{ cookies: {} },
13
14
);
14
15
export default async function DocumentPage(props: {
15
16
params: { doc_id: string };
···
20
21
<ReplicacheProvider name={props.params.doc_id} initialFacts={initialFacts}>
21
22
<div className="text-blue-400">doc_id: {props.params.doc_id}</div>
22
23
<AddBlock entityID={props.params.doc_id} />
24
+
<AddImageBlock entityID={props.params.doc_id} />
23
25
<Blocks entityID={props.params.doc_id} />
24
26
</ReplicacheProvider>
25
27
);
+2
app/layout.tsx
+2
app/layout.tsx
···
1
1
import { InitialPageLoad } from "../components/InitialPageLoadProvider";
2
+
import { ServiceWorker } from "../components/ServiceWorker";
2
3
import "./globals.css";
3
4
import localFont from "next/font/local";
4
5
···
23
24
return (
24
25
<html lang="en" className={`${quattro.variable}`}>
25
26
<body>
27
+
<ServiceWorker />
26
28
<InitialPageLoad>{children}</InitialPageLoad>
27
29
</body>
28
30
</html>
+11
components/ServiceWorker.tsx
+11
components/ServiceWorker.tsx
+22
-1
package-lock.json
+22
-1
package-lock.json
···
17
17
"@react-stately/color": "^3.6.0",
18
18
"@spectrum-css/colorarea": "^5.1.0",
19
19
"@spectrum-css/colorhandle": "^8.1.0",
20
+
"@supabase/ssr": "^0.3.0",
20
21
"@supabase/supabase-js": "^2.43.2",
21
22
"@vercel/kv": "^1.0.1",
22
23
"base64-js": "^1.5.1",
···
5874
5875
"ws": "^8.14.2"
5875
5876
}
5876
5877
},
5878
+
"node_modules/@supabase/ssr": {
5879
+
"version": "0.3.0",
5880
+
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.3.0.tgz",
5881
+
"integrity": "sha512-lcVyQ7H6eumb2FB1Wa2N+jYWMfq6CFza3KapikT0fgttMQ+QvDgpNogx9jI8bZgKds+XFSMCojxFvFb+gwdbfA==",
5882
+
"dependencies": {
5883
+
"cookie": "^0.5.0",
5884
+
"ramda": "^0.29.0"
5885
+
},
5886
+
"peerDependencies": {
5887
+
"@supabase/supabase-js": "^2.33.1"
5888
+
}
5889
+
},
5877
5890
"node_modules/@supabase/storage-js": {
5878
5891
"version": "2.5.5",
5879
5892
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.5.5.tgz",
···
6958
6971
"version": "0.5.0",
6959
6972
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
6960
6973
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
6961
-
"dev": true,
6962
6974
"engines": {
6963
6975
"node": ">= 0.6"
6964
6976
}
···
11095
11107
"url": "https://feross.org/support"
11096
11108
}
11097
11109
]
11110
+
},
11111
+
"node_modules/ramda": {
11112
+
"version": "0.29.1",
11113
+
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.1.tgz",
11114
+
"integrity": "sha512-OfxIeWzd4xdUNxlWhgFazxsA/nl3mS4/jGZI5n00uWOoSSFRhC1b6gl6xvmzUamgmqELraWp0J/qqVlXYPDPyA==",
11115
+
"funding": {
11116
+
"type": "opencollective",
11117
+
"url": "https://opencollective.com/ramda"
11118
+
}
11098
11119
},
11099
11120
"node_modules/react": {
11100
11121
"version": "18.3.1",
+1
package.json
+1
package.json
+19
public/worker.js
+19
public/worker.js
···
1
+
self.addEventListener("fetch", (event) => {
2
+
console.log("Handling fetch event for", event.request.url);
3
+
4
+
event.respondWith(
5
+
caches.open("minilink-user-assets").then(async (cache) => {
6
+
return cache
7
+
.match(event.request)
8
+
.then((response) => {
9
+
if (response) {
10
+
return response;
11
+
}
12
+
return fetch(event.request.clone());
13
+
})
14
+
.catch((error) => {
15
+
throw error;
16
+
});
17
+
}),
18
+
);
19
+
});
+4
replicache/attributes.ts
+4
replicache/attributes.ts
+3
-6
replicache/index.tsx
+3
-6
replicache/index.tsx
···
6
6
import { mutations } from "./mutations";
7
7
import { Attributes } from "./attributes";
8
8
import { Push } from "./push";
9
-
import { createClient } from "@supabase/supabase-js";
10
-
import { Database } from "../supabase/database.types";
11
9
import { clientMutationContext } from "./clientMutationContext";
10
+
import { supabaseBrowserClient } from "../supabase/browserClient";
12
11
13
12
export type Fact<A extends keyof typeof Attributes> = {
14
13
id: string;
···
24
23
position: string;
25
24
value: string;
26
25
};
26
+
image: { type: "image"; src: string; height: number; width: number };
27
27
reference: { type: "reference"; value: string };
28
28
}[(typeof Attributes)[A]["type"]];
29
29
···
47
47
}) {
48
48
let [rep, setRep] = useState<null | Replicache<ReplicacheMutators>>(null);
49
49
useEffect(() => {
50
-
let supabase = createClient<Database>(
51
-
process.env.NEXT_PUBLIC_SUPABASE_API_URL as string,
52
-
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY as string,
53
-
);
50
+
let supabase = supabaseBrowserClient();
54
51
let newRep = new Replicache({
55
52
pushDelay: 500,
56
53
mutators: Object.fromEntries(
+9
supabase/browserClient.ts
+9
supabase/browserClient.ts
···
1
+
import { createBrowserClient } from "@supabase/ssr";
2
+
import { Database } from "./database.types";
3
+
4
+
export function supabaseBrowserClient() {
5
+
return createBrowserClient<Database>(
6
+
process.env.NEXT_PUBLIC_SUPABASE_API_URL!,
7
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
8
+
);
9
+
}