Skip to content

Commit b07601f

Browse files
committed
feat: Loom importer extension
1 parent 684a0e2 commit b07601f

Some content is hidden

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

49 files changed

+5127
-268
lines changed
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
.DS_Store
3+
dist
4+
dist-ssr
5+
*.local
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## Cap Loom Importer
2+
3+
This is a Chrome extension that allows you to import your Loom videos into Cap.
4+
5+
## Structure
6+
7+
```
8+
├── src
9+
│ ├── background.ts # Background script for handling auth
10+
│ ├── content_scripts
11+
│ │ └── main.tsx # Import UI injected on Loom's website
12+
│ ├── popup
13+
│ └── popup.tsx # Popup for the extension (shown when the extension is clicked)
14+
└── vite.config.ts
15+
```
16+
17+
## Development
18+
19+
Go to chrome://extensions/ and click "Load unpacked" and select the `dist` folder.

apps/loom-importer-extension/bun.lock

+255
Large diffs are not rendered by default.
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!-- Do not move this file's location, it must be in the root of the project -->
2+
<!DOCTYPE html>
3+
<html lang="en">
4+
<head>
5+
<meta charset="UTF-8" />
6+
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8+
<title>Cap Loom Importer</title>
9+
</head>
10+
<body>
11+
<div id="root"></div>
12+
<script type="module" src="/src/content_scripts/main.tsx"></script>
13+
</body>
14+
</html>
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "loom-importer-extension",
3+
"version": "0.0.0",
4+
"scripts": {
5+
"dev": "vite",
6+
"build": "tsc && vite build",
7+
"watch": "vite build --watch",
8+
"type-check": "tsc --noEmit",
9+
"dev:extension": "echo '⚡ Building in watch mode. Load the extension from the dist folder in Chrome extensions page (chrome://extensions/)' && pnpm watch",
10+
"preview": "vite preview"
11+
},
12+
"dependencies": {
13+
"@cap/ui": "workspace:^",
14+
"@cap/ui-solid": "workspace:^",
15+
"js-confetti": "^0.12.0",
16+
"react": "^18.2.0",
17+
"react-dom": "^18.2.0",
18+
"react-frame-component": "^5.2.1",
19+
"vite-tsconfig-paths": "^3.3.17",
20+
"zustand": "^5.0.3"
21+
},
22+
"devDependencies": {
23+
"@types/chrome": "^0.0.176",
24+
"@types/react": "^18.2.0",
25+
"@types/react-dom": "^18.2.0",
26+
"@vitejs/plugin-react": "^1.0.7",
27+
"hot-reload-extension-vite": "^1.0.13",
28+
"tailwindcss": "^3.4.16",
29+
"typescript": "^4.4.4",
30+
"vite": "^2.7.2"
31+
}
32+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!-- Do not move this file's location, it must be in the root of the project -->
2+
<!DOCTYPE html>
3+
<html lang="en">
4+
<head>
5+
<meta charset="UTF-8" />
6+
<title>Cap Loom Importer Popup</title>
7+
</head>
8+
<body>
9+
<div id="root"></div>
10+
<script type="module" src="/src/popup/popup.tsx"></script>
11+
</body>
12+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
plugins: {
3+
tailwindcss: {},
4+
autoprefixer: {},
5+
},
6+
};
Loading
Loading
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
(async () => {
2+
const app = document.createElement("div");
3+
app.id = "root";
4+
document.body.append(app);
5+
6+
const src = chrome?.runtime?.getURL("/react/main.js");
7+
await import(src);
8+
})();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"manifest_version": 3,
3+
"name": "Cap Loom Importer",
4+
"description": "Import your Loom data to Cap",
5+
"version": "1.0.0",
6+
"icons": {
7+
"16": "assets/images/icons/icon16.png",
8+
"48": "assets/images/icons/icon48.png",
9+
"128": "assets/images/icons/icon128.png"
10+
},
11+
"content_scripts": [
12+
{
13+
"matches": ["https://www.loom.com/*"],
14+
"js": ["/assets/js/initializeUI.js"],
15+
"run_at": "document_end",
16+
"all_frames": true
17+
}
18+
],
19+
"background": {
20+
"service_worker": "react/background.js",
21+
"type": "module"
22+
},
23+
"action": {
24+
"default_popup": "popup.html"
25+
},
26+
"web_accessible_resources": [
27+
{
28+
"resources": [
29+
"/react/main.js",
30+
"/react/vendor.js",
31+
"/react/main.css",
32+
"/react/jsx-runtime.js",
33+
"/react/client.js",
34+
"/react/urls.js"
35+
],
36+
"matches": ["https://www.loom.com/*"]
37+
}
38+
],
39+
"permissions": ["cookies", "storage", "tabs", "scripting"],
40+
"host_permissions": [
41+
"https://cap.so/*",
42+
"http://localhost:3000/*",
43+
"https://www.loom.com/*"
44+
]
45+
}
+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { LoomExportData } from "../types/loom";
2+
import { getApiBaseUrl } from "../utils/urls";
3+
4+
interface ApiResponse<T> {
5+
data: T;
6+
error?: string;
7+
}
8+
9+
interface UserResponse {
10+
user: User;
11+
expires: string;
12+
}
13+
interface User {
14+
name: string;
15+
email: string;
16+
image: string;
17+
id: string;
18+
}
19+
20+
interface LoomImportResponse {
21+
success: boolean;
22+
message: string;
23+
}
24+
25+
interface Workspace {
26+
id: string;
27+
name: string;
28+
ownerId: string;
29+
metadata: null;
30+
allowedEmailDomain: null;
31+
customDomain: null;
32+
domainVerified: null;
33+
createdAt: string;
34+
updatedAt: string;
35+
workosOrganizationId: null;
36+
workosConnectionId: null;
37+
}
38+
39+
interface WorkspaceResponse {
40+
workspaces: Workspace[];
41+
}
42+
43+
export class CapApi {
44+
private baseUrl = getApiBaseUrl();
45+
private headers: HeadersInit = {
46+
accept: "*/*",
47+
"content-type": "application/json",
48+
};
49+
50+
private async getAuthToken(): Promise<string | null> {
51+
return new Promise((resolve) => {
52+
chrome.runtime.sendMessage({ action: "getAuthStatus" }, (response) => {
53+
resolve(response?.token || null);
54+
});
55+
});
56+
}
57+
58+
private async getHeaders(): Promise<HeadersInit> {
59+
const token = await this.getAuthToken();
60+
return {
61+
...this.headers,
62+
...(token ? { Authorization: `Bearer ${token}` } : {}),
63+
};
64+
}
65+
66+
private async request<T>(
67+
endpoint: string,
68+
options: RequestInit = {}
69+
): Promise<ApiResponse<T>> {
70+
try {
71+
const headers = await this.getHeaders();
72+
const response = await fetch(`${this.baseUrl}${endpoint}`, {
73+
...options,
74+
headers: {
75+
...headers,
76+
...options.headers,
77+
},
78+
});
79+
80+
const data = await response.json();
81+
82+
if (!response.ok) {
83+
throw new Error(data.error || "API request failed");
84+
}
85+
86+
return { data: data as T };
87+
} catch (error) {
88+
return {
89+
data: {} as T,
90+
error:
91+
error instanceof Error ? error.message : "Unknown error occurred",
92+
};
93+
}
94+
}
95+
96+
public async getUser(): Promise<UserResponse> {
97+
const response = await this.request<UserResponse>("/auth/session");
98+
return response.data;
99+
}
100+
101+
/**
102+
* Sends imported Loom data to Cap.so
103+
* @param loomData The exported Loom data to import into Cap.so
104+
* @returns Response with import status
105+
*/
106+
public async sendLoomData(
107+
loomData: LoomExportData
108+
): Promise<LoomImportResponse> {
109+
const response = await this.request<LoomImportResponse>("/import/loom", {
110+
method: "POST",
111+
body: JSON.stringify(loomData),
112+
});
113+
114+
if (response.error) {
115+
return {
116+
success: false,
117+
message: response.error,
118+
};
119+
}
120+
121+
return response.data;
122+
}
123+
124+
public async getWorkspaceDetails(): Promise<WorkspaceResponse> {
125+
const response = await this.request<WorkspaceResponse>(
126+
"/settings/workspace/details"
127+
);
128+
return response.data;
129+
}
130+
}

0 commit comments

Comments
 (0)