this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

front(new): add ridiculous /new page

bruh the assignment is to be finished soon I need to have something
working lol

Clément 9c7fe5e4 7c913989

+345 -24
+1
app/package.json
··· 62 62 "thememirror": "^2.0.1", 63 63 "ts-pattern": "^5.9.0", 64 64 "typescript": "^6.0.3", 65 + "valibot": "^1.4.0", 65 66 "vite": "^8.0.10", 66 67 "vite-plugin-lucide-preprocess": "^1.4.10", 67 68 "vite-plugin-solid": "^2.11.12",
+2 -24
app/src/contexts/auth.tsx
··· 4 4 import { createContext, useContext } from 'solid-js'; 5 5 import type { JSX } from 'solid-js'; 6 6 7 - import { prisma } from '~/server/db.server'; 8 - 9 - import { useAppSession } from '$/session'; 7 + import { requireUser } from '~/server/auth.server'; 10 8 11 9 type AuthContextType = { 12 10 did: () => Did | null; ··· 15 13 }; 16 14 17 15 export const getCurrentUserFn = createServerFn({ method: 'GET' }).handler( 18 - async () => { 19 - const session = await useAppSession(); 20 - 21 - if (!session.data.token) { 22 - session.clear(); 23 - return { did: null }; 24 - } 25 - 26 - const user = await prisma.user.findFirst({ 27 - where: { 28 - whitelisted: true, 29 - sessions: { some: { token: session.data.token } }, 30 - }, 31 - }); 32 - 33 - if (!user?.did) { 34 - session.clear(); 35 - return { did: null }; 36 - } 37 - return { did: user.did }; 38 - }, 16 + async () => requireUser(), 39 17 ); 40 18 41 19 const AuthContext = createContext<AuthContextType | undefined>(undefined);
+21
app/src/routeTree.gen.ts
··· 10 10 11 11 import { Route as rootRouteImport } from './routes/__root' 12 12 import { Route as SettingsRouteImport } from './routes/settings' 13 + import { Route as NewRouteImport } from './routes/new' 13 14 import { Route as LeaderboardRouteImport } from './routes/leaderboard' 14 15 import { Route as FriendsRouteImport } from './routes/friends' 15 16 import { Route as ExploreRouteImport } from './routes/explore' ··· 20 21 const SettingsRoute = SettingsRouteImport.update({ 21 22 id: '/settings', 22 23 path: '/settings', 24 + getParentRoute: () => rootRouteImport, 25 + } as any) 26 + const NewRoute = NewRouteImport.update({ 27 + id: '/new', 28 + path: '/new', 23 29 getParentRoute: () => rootRouteImport, 24 30 } as any) 25 31 const LeaderboardRoute = LeaderboardRouteImport.update({ ··· 58 64 '/explore': typeof ExploreRoute 59 65 '/friends': typeof FriendsRoute 60 66 '/leaderboard': typeof LeaderboardRoute 67 + '/new': typeof NewRoute 61 68 '/settings': typeof SettingsRoute 62 69 '/oauth/callback': typeof OauthCallbackRoute 63 70 '/oauth/jwks': typeof OauthJwksRoute ··· 67 74 '/explore': typeof ExploreRoute 68 75 '/friends': typeof FriendsRoute 69 76 '/leaderboard': typeof LeaderboardRoute 77 + '/new': typeof NewRoute 70 78 '/settings': typeof SettingsRoute 71 79 '/oauth/callback': typeof OauthCallbackRoute 72 80 '/oauth/jwks': typeof OauthJwksRoute ··· 77 85 '/explore': typeof ExploreRoute 78 86 '/friends': typeof FriendsRoute 79 87 '/leaderboard': typeof LeaderboardRoute 88 + '/new': typeof NewRoute 80 89 '/settings': typeof SettingsRoute 81 90 '/oauth/callback': typeof OauthCallbackRoute 82 91 '/oauth/jwks': typeof OauthJwksRoute ··· 88 97 | '/explore' 89 98 | '/friends' 90 99 | '/leaderboard' 100 + | '/new' 91 101 | '/settings' 92 102 | '/oauth/callback' 93 103 | '/oauth/jwks' ··· 97 107 | '/explore' 98 108 | '/friends' 99 109 | '/leaderboard' 110 + | '/new' 100 111 | '/settings' 101 112 | '/oauth/callback' 102 113 | '/oauth/jwks' ··· 106 117 | '/explore' 107 118 | '/friends' 108 119 | '/leaderboard' 120 + | '/new' 109 121 | '/settings' 110 122 | '/oauth/callback' 111 123 | '/oauth/jwks' ··· 116 128 ExploreRoute: typeof ExploreRoute 117 129 FriendsRoute: typeof FriendsRoute 118 130 LeaderboardRoute: typeof LeaderboardRoute 131 + NewRoute: typeof NewRoute 119 132 SettingsRoute: typeof SettingsRoute 120 133 OauthCallbackRoute: typeof OauthCallbackRoute 121 134 OauthJwksRoute: typeof OauthJwksRoute ··· 128 141 path: '/settings' 129 142 fullPath: '/settings' 130 143 preLoaderRoute: typeof SettingsRouteImport 144 + parentRoute: typeof rootRouteImport 145 + } 146 + '/new': { 147 + id: '/new' 148 + path: '/new' 149 + fullPath: '/new' 150 + preLoaderRoute: typeof NewRouteImport 131 151 parentRoute: typeof rootRouteImport 132 152 } 133 153 '/leaderboard': { ··· 180 200 ExploreRoute: ExploreRoute, 181 201 FriendsRoute: FriendsRoute, 182 202 LeaderboardRoute: LeaderboardRoute, 203 + NewRoute: NewRoute, 183 204 SettingsRoute: SettingsRoute, 184 205 OauthCallbackRoute: OauthCallbackRoute, 185 206 OauthJwksRoute: OauthJwksRoute,
+271
app/src/routes/new.tsx
··· 1 + import * as Problem from '#/at/compiles/alpha/problem'; 2 + import '@atcute/atproto'; 3 + import { parse } from '@atcute/lexicons'; 4 + import { createForm } from '@tanstack/solid-form'; 5 + import { createFileRoute, redirect } from '@tanstack/solid-router'; 6 + import { createServerFn, useServerFn } from '@tanstack/solid-start'; 7 + import { Show } from 'solid-js'; 8 + import * as v from 'valibot'; 9 + 10 + import { getCurrentUserFn } from '~/contexts/auth'; 11 + import { requireAuthedRpc } from '~/server/auth.server'; 12 + 13 + import { Button } from '@/Button'; 14 + 15 + export const createProblemFn = createServerFn({ method: 'POST' }) 16 + .inputValidator( 17 + v.object({ 18 + title: v.pipe(v.string(), v.minLength(5), v.maxLength(50_000)), 19 + description: v.pipe(v.string(), v.maxLength(10_000)), 20 + initialSolution: v.pipe(v.string(), v.minLength(1), v.maxLength(50_000)), 21 + tests: v.pipe(v.string(), v.minLength(1), v.maxLength(50_000)), 22 + starterCode: v.pipe(v.string(), v.minLength(1), v.maxLength(50_000)), 23 + }), 24 + ) 25 + .handler(async ({ data }) => { 26 + const { user, rpc } = await requireAuthedRpc(); 27 + 28 + const recordInput: Problem.Main = { 29 + $type: 'at.compiles.alpha.problem', 30 + language: 'python', 31 + ...data, 32 + }; 33 + const record = parse(Problem.mainSchema, recordInput); 34 + 35 + const res = await rpc.post('com.atproto.repo.createRecord', { 36 + input: { 37 + repo: user.did, 38 + collection: 'at.compiles.alpha.problem', 39 + record, 40 + }, 41 + }); 42 + 43 + if (!res.ok) throw new Error(res.data.error); 44 + return { uri: res.data.uri, cid: res.data.cid }; 45 + }); 46 + 47 + export const Route = createFileRoute('/new')({ 48 + beforeLoad: async () => { 49 + const { did } = await getCurrentUserFn(); 50 + if (!did) throw redirect({ to: '/' }); 51 + }, 52 + component: RouteComponent, 53 + }); 54 + 55 + type FormValues = { 56 + title: string; 57 + description: string; 58 + initialSolution: string; 59 + tests: string; 60 + starterCode: string; 61 + }; 62 + 63 + const inputClass = 64 + 'border border-border-input bg-bg-input rounded-input px-3 py-2 focus-ring'; 65 + const labelClass = 'flex flex-col gap-1'; 66 + 67 + function RouteComponent() { 68 + const createProblem = useServerFn(createProblemFn); 69 + 70 + const form = createForm(() => ({ 71 + defaultValues: { 72 + title: 'Two Sum', 73 + description: 74 + 'Iterate over the list of numbers `nums`, and find the first two numbers which their sums adds up to `target`. There can ony be one solution', 75 + initialSolution: `\ 76 + def twoSum(self, nums: list[int], target: int) -> list[int]: 77 + for i1, n1 in enumerate(nums[:-1]): 78 + for i2, n2 in enumerate(nums[i1+1:]): 79 + if n1 + n2 == target: 80 + return [i1, i2+i1+1]`, 81 + tests: `\ 82 + def test_one(): 83 + expected = [0, 1] 84 + got = twoSum([2, 7, 9], 9) 85 + assert expected == got 86 + 87 + def test_two(): 88 + expected = [1, 2] 89 + got = twoSum([9, 2, 7], 9) 90 + assert expected == got 91 + 92 + def test_three(): 93 + expected = [0, 1] 94 + got = twoSum([2, 2, 2], 4) 95 + assert expected == got 96 + `, 97 + starterCode: `\ 98 + def twoSum(self, nums: list[int], target: int) -> list[int]: 99 + ... 100 + `, 101 + } as FormValues, 102 + onSubmit: async ({ value, formApi }) => { 103 + try { 104 + await createProblem({ data: value }); 105 + } catch (e) { 106 + formApi.setErrorMap({ 107 + onSubmit: { 108 + form: e instanceof Error ? e.message : 'unknown error', 109 + fields: {}, 110 + }, 111 + }); 112 + } 113 + }, 114 + })); 115 + 116 + return ( 117 + <main class="mx-auto w-full max-w-2xl p-4"> 118 + <h1 class="text-2xl font-semibold mb-4">create a new problem</h1> 119 + <form 120 + class="flex flex-col gap-4" 121 + onSubmit={(e) => { 122 + e.preventDefault(); 123 + e.stopPropagation(); 124 + form.handleSubmit(); 125 + }} 126 + > 127 + <form.Subscribe selector={(s) => s.errorMap.onSubmit}> 128 + {(error) => ( 129 + <Show when={error()}> 130 + {(msg) => ( 131 + <p class="text-red-400 text-sm" role="alert"> 132 + {msg() as string} 133 + </p> 134 + )} 135 + </Show> 136 + )} 137 + </form.Subscribe> 138 + 139 + <form.Field 140 + name="title" 141 + validators={{ 142 + onSubmit: ({ value }) => (value.trim() ? undefined : 'required'), 143 + }} 144 + > 145 + {(field) => ( 146 + <label class={labelClass}> 147 + <span>title</span> 148 + <input 149 + class={inputClass} 150 + value={field().state.value} 151 + onBlur={field().handleBlur} 152 + onInput={(e) => field().handleChange(e.currentTarget.value)} 153 + maxLength={200} 154 + /> 155 + <p class="text-red-400 text-sm" role="alert"> 156 + {field().state.meta.errors} 157 + </p> 158 + </label> 159 + )} 160 + </form.Field> 161 + 162 + <form.Field 163 + name="description" 164 + validators={{ 165 + onSubmit: ({ value }) => (value.trim() ? undefined : 'required'), 166 + }} 167 + > 168 + {(field) => ( 169 + <label class={labelClass}> 170 + <span>description</span> 171 + <textarea 172 + class={`${inputClass} min-h-32`} 173 + value={field().state.value} 174 + onBlur={field().handleBlur} 175 + onInput={(e) => field().handleChange(e.currentTarget.value)} 176 + /> 177 + <p class="text-red-400 text-sm" role="alert"> 178 + {field().state.meta.errors} 179 + </p> 180 + </label> 181 + )} 182 + </form.Field> 183 + 184 + <form.Field 185 + name="initialSolution" 186 + validators={{ 187 + onSubmit: ({ value }) => (value.trim() ? undefined : 'required'), 188 + }} 189 + > 190 + {(field) => ( 191 + <label class={labelClass}> 192 + <span>initial solution</span> 193 + <textarea 194 + class={`${inputClass} min-h-32`} 195 + value={field().state.value} 196 + onBlur={field().handleBlur} 197 + onInput={(e) => field().handleChange(e.currentTarget.value)} 198 + /> 199 + <p class="text-red-400 text-sm" role="alert"> 200 + {field().state.meta.errors} 201 + </p> 202 + </label> 203 + )} 204 + </form.Field> 205 + 206 + <form.Field 207 + name="tests" 208 + validators={{ 209 + onSubmit: ({ value }) => (value.trim() ? undefined : 'required'), 210 + }} 211 + > 212 + {(field) => ( 213 + <label class={labelClass}> 214 + <span>tests cases</span> 215 + <textarea 216 + class={`${inputClass} min-h-32`} 217 + value={field().state.value} 218 + onBlur={field().handleBlur} 219 + onInput={(e) => field().handleChange(e.currentTarget.value)} 220 + /> 221 + <p class="text-red-400 text-sm" role="alert"> 222 + {field().state.meta.errors} 223 + </p> 224 + </label> 225 + )} 226 + </form.Field> 227 + 228 + <form.Field 229 + name="starterCode" 230 + validators={{ 231 + onSubmit: ({ value }) => (value.trim() ? undefined : 'required'), 232 + }} 233 + > 234 + {(field) => ( 235 + <label class={labelClass}> 236 + <span>starter code</span> 237 + <textarea 238 + class={`${inputClass} min-h-32`} 239 + value={field().state.value} 240 + onBlur={field().handleBlur} 241 + onInput={(e) => field().handleChange(e.currentTarget.value)} 242 + /> 243 + <p class="text-red-400 text-sm" role="alert"> 244 + {field().state.meta.errors} 245 + </p> 246 + </label> 247 + )} 248 + </form.Field> 249 + 250 + <div class="flex justify-end"> 251 + <form.Subscribe 252 + selector={({ canSubmit, isSubmitting }) => ({ 253 + canSubmit, 254 + isSubmitting, 255 + })} 256 + > 257 + {(p) => ( 258 + <Button 259 + variant="contrast" 260 + type="submit" 261 + disabled={!p().canSubmit || p().isSubmitting} 262 + > 263 + {p().isSubmitting ? 'submitting...' : 'create problem'} 264 + </Button> 265 + )} 266 + </form.Subscribe> 267 + </div> 268 + </form> 269 + </main> 270 + ); 271 + }
+35
app/src/server/auth.server.ts
··· 1 + import { Client } from '@atcute/client'; 2 + 3 + import { useAppSession } from '~/lib/session'; 4 + 5 + import { prisma } from './db.server'; 6 + import { getOAuthClient } from './oauth-client.server'; 7 + 8 + export async function requireUser() { 9 + const session = await useAppSession(); 10 + 11 + if (!session.data.token) { 12 + session.clear(); 13 + return { did: null }; 14 + } 15 + 16 + const user = await prisma.user.findFirst({ 17 + where: { 18 + whitelisted: true, 19 + sessions: { some: { token: session.data.token } }, 20 + }, 21 + }); 22 + 23 + if (!user?.did) { 24 + session.clear(); 25 + return { did: null }; 26 + } 27 + return { did: user.did }; 28 + } 29 + 30 + export async function requireAuthedRpc() { 31 + const user = await requireUser(); 32 + if (!user.did) throw new Error('not logged in'); 33 + const session = await getOAuthClient().restore(user.did, { refresh: 'auto' }); 34 + return { user, rpc: new Client({ handler: session }) }; 35 + }
+15
pnpm-lock.yaml
··· 142 142 typescript: 143 143 specifier: ^6.0.3 144 144 version: 6.0.3 145 + valibot: 146 + specifier: ^1.4.0 147 + version: 1.4.0(typescript@6.0.3) 145 148 vite: 146 149 specifier: ^8.0.10 147 150 version: 8.0.10(@types/node@24.10.12)(esbuild@0.27.3)(jiti@2.6.1) ··· 3270 3273 3271 3274 valibot@1.2.0: 3272 3275 resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} 3276 + peerDependencies: 3277 + typescript: '>=5' 3278 + peerDependenciesMeta: 3279 + typescript: 3280 + optional: true 3281 + 3282 + valibot@1.4.0: 3283 + resolution: {integrity: sha512-iC/x7fVcSyOwlm/VSt7RlHnzNGLGvR9GnxdifUeWoCJo0q4ZZvrVkIHC6faTlkxG47I2Y4UrFquPuVHCrOnrLg==} 3273 3284 peerDependencies: 3274 3285 typescript: '>=5' 3275 3286 peerDependenciesMeta: ··· 6936 6947 punycode: 2.3.1 6937 6948 6938 6949 valibot@1.2.0(typescript@6.0.3): 6950 + optionalDependencies: 6951 + typescript: 6.0.3 6952 + 6953 + valibot@1.4.0(typescript@6.0.3): 6939 6954 optionalDependencies: 6940 6955 typescript: 6.0.3 6941 6956