tangled
alpha
login
or
join now
roost.moe
/
recipes.blue
2
fork
atom
The recipes.blue monorepo
recipes.blue
recipes
appview
atproto
2
fork
atom
overview
issues
1
pulls
pipelines
feat: secondary nav with new recipe link
Hayden Young
1 year ago
fcc9f4cc
fbf54975
+105
-69
7 changed files
expand all
collapse all
unified
split
apps
api
src
recipes
index.ts
web
package.json
src
components
app-sidebar.tsx
nav-user-opts.tsx
nav-user.tsx
queries
recipe.ts
pnpm-lock.yaml
+1
-50
apps/api/src/recipes/index.ts
···
1
1
import { Hono } from "hono";
2
2
-
import { getDidDoc, getPdsUrl, RecipeCollection, RecipeRecord } from "@cookware/lexicons";
3
3
-
import { TID } from "@atproto/common";
4
4
-
import { verifyJwt } from "../util/jwt.js";
5
5
-
import { XRPCError } from "../util/xrpc.js";
6
2
7
3
export const recipeApp = new Hono();
8
4
9
9
-
recipeApp.post('/', async ctx => {
10
10
-
const authz = ctx.req.header('Authorization');
11
11
-
if (!authz || !authz.startsWith('Bearer ')) {
12
12
-
throw new XRPCError('this endpoint requires authentication', 'authz_required', 401);
13
13
-
}
14
14
-
15
15
-
const serviceJwt = await verifyJwt(
16
16
-
authz.split(' ')[1]!,
17
17
-
'did:web:recipes.blue#api',
18
18
-
null,
19
19
-
async (iss, forceRefresh) => {
20
20
-
console.log(iss);
21
21
-
return '';
22
22
-
},
23
23
-
);
24
24
-
25
25
-
//const agent = await getSessionAgent(ctx);
26
26
-
//if (!agent) {
27
27
-
// ctx.status(401);
28
28
-
// return ctx.json({
29
29
-
// error: 'unauthenticated',
30
30
-
// message: 'You must be authenticated to access this resource.',
31
31
-
// });
32
32
-
//}
33
33
-
//
34
34
-
//const body = await ctx.req.json();
35
35
-
//const { data: record, success, error } = RecipeRecord.safeParse(body);
36
36
-
//if (!success) {
37
37
-
// ctx.status(400);
38
38
-
// return ctx.json({
39
39
-
// error: 'invalid_recipe',
40
40
-
// message: error.message,
41
41
-
// fields: error.formErrors,
42
42
-
// });
43
43
-
//}
44
44
-
//
45
45
-
//const res = await agent.com.atproto.repo.putRecord({
46
46
-
// repo: agent.assertDid,
47
47
-
// collection: RecipeCollection,
48
48
-
// record: record,
49
49
-
// rkey: TID.nextStr(),
50
50
-
// validate: false,
51
51
-
//});
52
52
-
//
53
53
-
//return ctx.json(res.data);
54
54
-
});
5
5
+
recipeApp.post('/', async ctx => {});
+2
apps/web/package.json
···
12
12
"dependencies": {
13
13
"@atcute/client": "^2.0.6",
14
14
"@atcute/oauth-browser-client": "^1.0.7",
15
15
+
"@atproto/common": "^0.4.5",
16
16
+
"@atproto/common-web": "^0.3.1",
15
17
"@dnd-kit/core": "^6.3.1",
16
18
"@dnd-kit/modifiers": "^9.0.0",
17
19
"@dnd-kit/sortable": "^10.0.0",
+2
apps/web/src/components/app-sidebar.tsx
···
17
17
SidebarMenuItem,
18
18
SidebarRail,
19
19
} from "@/components/ui/sidebar"
20
20
+
import { NavUserOpts } from "./nav-user-opts"
20
21
21
22
const data = {
22
23
navMain: [
···
76
77
</SidebarHeader>
77
78
<SidebarContent>
78
79
<NavMain items={data.navMain} />
80
80
+
<NavUserOpts />
79
81
</SidebarContent>
80
82
<SidebarFooter>
81
83
<NavUser />
+67
apps/web/src/components/nav-user-opts.tsx
···
1
1
+
"use client"
2
2
+
3
3
+
import {
4
4
+
SidebarGroup,
5
5
+
SidebarGroupContent,
6
6
+
SidebarMenu,
7
7
+
SidebarMenuButton,
8
8
+
SidebarMenuItem,
9
9
+
} from "@/components/ui/sidebar"
10
10
+
import { useAuth } from "@/state/auth"
11
11
+
import { Link } from "@tanstack/react-router";
12
12
+
import { LifeBuoy, Pencil, Send } from "lucide-react";
13
13
+
14
14
+
export function NavUserOpts() {
15
15
+
const { isLoggedIn } = useAuth();
16
16
+
17
17
+
if (!isLoggedIn) {
18
18
+
return (
19
19
+
<SidebarGroup className="mt-auto">
20
20
+
<SidebarGroupContent>
21
21
+
<SidebarMenu>
22
22
+
<AlwaysItems />
23
23
+
</SidebarMenu>
24
24
+
</SidebarGroupContent>
25
25
+
</SidebarGroup>
26
26
+
);
27
27
+
}
28
28
+
29
29
+
return (
30
30
+
<SidebarGroup className="mt-auto">
31
31
+
<SidebarGroupContent>
32
32
+
<SidebarMenu>
33
33
+
<AlwaysItems />
34
34
+
<SidebarMenuItem>
35
35
+
<SidebarMenuButton asChild size="sm">
36
36
+
<Link to="/recipes/new">
37
37
+
<Pencil />
38
38
+
<span>New recipe</span>
39
39
+
</Link>
40
40
+
</SidebarMenuButton>
41
41
+
</SidebarMenuItem>
42
42
+
</SidebarMenu>
43
43
+
</SidebarGroupContent>
44
44
+
</SidebarGroup>
45
45
+
)
46
46
+
}
47
47
+
48
48
+
const AlwaysItems = () => (
49
49
+
<>
50
50
+
<SidebarMenuItem>
51
51
+
<SidebarMenuButton asChild size="sm">
52
52
+
<a href="https://github.com/recipes-blue/recipes.blue/issues/new">
53
53
+
<LifeBuoy />
54
54
+
<span>Support</span>
55
55
+
</a>
56
56
+
</SidebarMenuButton>
57
57
+
</SidebarMenuItem>
58
58
+
<SidebarMenuItem>
59
59
+
<SidebarMenuButton asChild size="sm">
60
60
+
<a href="https://github.com/recipes-blue/recipes.blue/discussions">
61
61
+
<Send />
62
62
+
<span>Feedback</span>
63
63
+
</a>
64
64
+
</SidebarMenuButton>
65
65
+
</SidebarMenuItem>
66
66
+
</>
67
67
+
);
+1
-8
apps/web/src/components/nav-user.tsx
···
100
100
</div>
101
101
</DropdownMenuLabel>
102
102
<DropdownMenuSeparator />
103
103
-
<DropdownMenuGroup>
104
104
-
<DropdownMenuItem>
105
105
-
<BadgeCheck />
106
106
-
Account
107
107
-
</DropdownMenuItem>
108
108
-
</DropdownMenuGroup>
109
109
-
<DropdownMenuSeparator />
110
110
-
<DropdownMenuItem>
103
103
+
<DropdownMenuItem className="cursor-pointer" onClick={() => agent.signOut()}>
111
104
<LogOut />
112
105
Log out
113
106
</DropdownMenuItem>
+22
-7
apps/web/src/queries/recipe.ts
···
1
1
import { useXrpc } from "@/hooks/use-xrpc";
2
2
-
import { SERVER_URL } from "@/lib/utils";
2
2
+
import { useAuth } from "@/state/auth";
3
3
import { XRPC, XRPCError } from "@atcute/client";
4
4
-
import { Recipe } from "@cookware/lexicons";
4
4
+
import { Recipe, RecipeCollection } from "@cookware/lexicons";
5
5
import { queryOptions, useMutation, useQuery } from "@tanstack/react-query";
6
6
-
import { notFound } from "@tanstack/react-router";
7
7
-
import axios from "axios";
6
6
+
import { notFound, useLocation, useRouter } from "@tanstack/react-router";
8
7
import { UseFormReturn } from "react-hook-form";
8
8
+
import { TID } from '@atproto/common-web';
9
9
10
10
const RQKEY_ROOT = 'posts';
11
11
export const RQKEY = (cursor: string, did: string, rkey: string) => [RQKEY_ROOT, cursor, did, rkey];
···
48
48
};
49
49
50
50
export const useNewRecipeMutation = (form: UseFormReturn<Recipe>) => {
51
51
+
const { agent } = useAuth();
52
52
+
const rpc = useXrpc();
51
53
return useMutation({
52
54
mutationKey: ['recipes.new'],
53
55
mutationFn: async ({ recipe }: { recipe: Recipe }) => {
54
54
-
const res = await axios.post(`https://${SERVER_URL}/api/recipes`, recipe);
55
55
-
return res.data;
56
56
+
const rkey = TID.nextStr();
57
57
+
const res = await rpc.call(`com.atproto.repo.createRecord`, {
58
58
+
data: {
59
59
+
repo: agent?.session.info.sub as `did:${string}`,
60
60
+
record: recipe,
61
61
+
collection: RecipeCollection,
62
62
+
rkey: rkey,
63
63
+
},
64
64
+
});
65
65
+
return {
66
66
+
rkey: rkey,
67
67
+
resp: res.data
68
68
+
};
56
69
},
57
70
onError: (error) => {
58
58
-
console.error(error);
59
71
form.setError('title', error);
72
72
+
},
73
73
+
onSuccess: ({ rkey }) => {
74
74
+
window.location.assign(`/recipes/${agent?.sub}/${rkey}`);
60
75
},
61
76
});
62
77
};
+10
-4
pnpm-lock.yaml
···
52
52
version: 4.0.8
53
53
drizzle-orm:
54
54
specifier: ^0.37.0
55
55
-
version: 0.37.0(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0)
55
55
+
version: 0.37.0(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0)
56
56
hono:
57
57
specifier: ^4.6.12
58
58
version: 4.6.12
···
131
131
version: 4.0.8
132
132
drizzle-orm:
133
133
specifier: ^0.37.0
134
134
-
version: 0.37.0(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0)
134
134
+
version: 0.37.0(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0)
135
135
pino:
136
136
specifier: ^9.5.0
137
137
version: 9.5.0
···
181
181
'@atcute/oauth-browser-client':
182
182
specifier: ^1.0.7
183
183
version: 1.0.7
184
184
+
'@atproto/common':
185
185
+
specifier: ^0.4.5
186
186
+
version: 0.4.5
187
187
+
'@atproto/common-web':
188
188
+
specifier: ^0.3.1
189
189
+
version: 0.3.1
184
190
'@dnd-kit/core':
185
191
specifier: ^6.3.1
186
192
version: 6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
···
334
340
version: 0.14.0(bufferutil@4.0.8)
335
341
drizzle-orm:
336
342
specifier: ^0.37.0
337
337
-
version: 0.37.0(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0)
343
343
+
version: 0.37.0(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0)
338
344
zod:
339
345
specifier: ^3.23.8
340
346
version: 3.23.8
···
6665
6671
transitivePeerDependencies:
6666
6672
- supports-color
6667
6673
6668
6668
-
drizzle-orm@0.37.0(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0):
6674
6674
+
drizzle-orm@0.37.0(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0):
6669
6675
optionalDependencies:
6670
6676
'@libsql/client': 0.14.0(bufferutil@4.0.8)
6671
6677
'@opentelemetry/api': 1.9.0