Compare commits

...

12 Commits

Author SHA1 Message Date
c84a583992 New: make the shortCode column redirect to the url
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m31s
2026-06-04 18:22:25 +02:00
ea12e01eed New: Table with List of redirections
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m7s
2026-06-04 17:31:50 +02:00
e0a74eeb9e Reset the form and refresh the list below
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m11s
2026-06-04 17:17:36 +02:00
35043fc9e3 Remove unused imports: shorten.tsx 2026-06-04 16:22:33 +02:00
42ceb798d1 New: Create URL Redirection component & Ability to use Connect-Mutations
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m16s
2026-06-04 16:21:09 +02:00
5cf8b26737 Force old nitro version
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m8s
2026-06-04 14:32:10 +02:00
a85950d6ef Logging: Server.ts fetch
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m36s
2026-06-04 14:23:41 +02:00
d3bb5c2be1 Wrap Transport Provider arround QueryClient Provider 2026-06-04 14:23:29 +02:00
c068e243b0 Fix transport
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m9s
2026-06-03 16:10:49 +02:00
81ae2650b2 allow esbuild builds
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m18s
2026-06-03 11:36:01 +00:00
5519c016aa ConnectRPC: Initialize buf workspace, generate ts files, use using connect-query
Some checks failed
Build and Push Docker Image / build (push) Failing after 4m20s
2026-06-03 13:24:17 +02:00
30a52c800a New: proto submodule 2026-06-03 13:22:28 +02:00
21 changed files with 6002 additions and 342 deletions

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "proto"]
path = proto
url = https://git.kocoder.xyz/vt/proto

18
buf.gen.yaml Normal file
View File

@@ -0,0 +1,18 @@
version: v2
plugins:
- remote: buf.build/bufbuild/es:v2.12.0
out: src/gen
opt: target=ts
include_imports: true
- remote: buf.build/connectrpc/query-es:v2.2.0
out: src/gen
opt:
- target=ts
managed:
enabled: true
override:
- file_option: go_package_prefix
value: git.kocoder.xyz/vt/shortener/internal
disable:
- file_option: go_package
module: buf.build/bufbuild/protovalidate

6
buf.lock Normal file
View File

@@ -0,0 +1,6 @@
# Generated by buf. DO NOT EDIT.
version: v2
deps:
- name: buf.build/bufbuild/protovalidate
commit: 50325440f8f24053b047484a6bf60b76
digest: b5:74cb6f5c0853c3c10aafc701614194bbd63326bdb8ef4068214454b8894b03ba4113e04b3a33a8321cdf05336e37db4dc14a5e2495db8462566914f36086ba31

9
buf.yaml Normal file
View File

@@ -0,0 +1,9 @@
version: v2
deps:
- buf.build/bufbuild/protovalidate
lint:
use:
- STANDARD
breaking:
use:
- FILE

View File

@@ -13,6 +13,11 @@
"check": "biome check" "check": "biome check"
}, },
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^2.12.0",
"@bufbuild/protovalidate": "^1.2.0",
"@connectrpc/connect": "^2.1.1",
"@connectrpc/connect-query": "^2.2.0",
"@connectrpc/connect-web": "^2.1.1",
"@fontsource-variable/noto-sans": "^5.2.10", "@fontsource-variable/noto-sans": "^5.2.10",
"@fontsource/inter": "^5.1.1", "@fontsource/inter": "^5.1.1",
"@kobalte/core": "^0.13.11", "@kobalte/core": "^0.13.11",
@@ -33,7 +38,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^1.16.0", "lucide-react": "^1.16.0",
"nitro": "npm:nitro-nightly@latest", "nitro": "3.0.260429-beta",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"shadcn": "^4.7.0", "shadcn": "^4.7.0",
"solid-js": "^1.9.12", "solid-js": "^1.9.12",
@@ -46,6 +51,7 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.4.5", "@biomejs/biome": "2.4.5",
"@bufbuild/buf": "^1.70.0",
"@solidjs/testing-library": "^0.8.10", "@solidjs/testing-library": "^0.8.10",
"@tanstack/devtools-vite": "latest", "@tanstack/devtools-vite": "latest",
"jsdom": "^28.1.0", "jsdom": "^28.1.0",

629
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,7 @@
allowBuilds: allowBuilds:
'@bufbuild/buf': true
esbuild: true
msw: true msw: true
onlyBuiltDependencies: onlyBuiltDependencies:
- esbuild - esbuild
- lightningcss - lightningcss

1
proto Submodule

Submodule proto added at 382266dac5

View File

@@ -1,13 +1,14 @@
import { Link } from '@tanstack/solid-router' import { Link } from "@tanstack/solid-router";
import TanStackQueryHeaderUser from '../integrations/tanstack-query/header-user.tsx' import TanStackQueryHeaderUser from "../integrations/tanstack-query/header-user.tsx";
export default function Header() { export default function Header() {
return ( return (
<div class="flex items-center justify-between px-4"> <div class="flex items-center justify-between px-4">
<Link to="/">Home</Link> <Link to="/">Home</Link>
<Link to="/todo">Todo</Link> <Link to="/todo">Todo</Link>
<Link to="/shorten">Shorten</Link>
<TanStackQueryHeaderUser /> <TanStackQueryHeaderUser />
</div> </div>
) );
} }

View File

@@ -0,0 +1,79 @@
import {
useConnectMutation,
getConnectQueryKey,
} from "~/integrations/connect-query/solid";
import {
createURLRedirection,
listURLRedirections,
} from "~/gen/proto/shorten/v1/shorten-ShortenService_connectquery";
import useAppForm from "../form/appform";
import { Button } from "../ui/button";
import { useQueryClient } from "@tanstack/solid-query";
const CreateRedirectionForm = () => {
const mutation = useConnectMutation(createURLRedirection);
const queryClient = useQueryClient();
const form = useAppForm(() => ({
defaultValues: {
url: "",
slug: "",
},
onSubmit: async (values) => {
await mutation.mutateAsync({
url: values.value.url,
shortCode: values.value.slug,
});
queryClient.invalidateQueries({
queryKey: getConnectQueryKey(listURLRedirections),
});
form.reset();
},
}));
return (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
>
<div class="bg-card p-2">
<h2 class="p-2 my-2">Create New Redirection</h2>
<div class="flex gap-x-2 p-2">
<div class="w-1/2">
<form.AppField
name="slug"
children={(field) => (
<field.TextField
name="slug"
label="Slug"
description={"The slug which should redirect"}
placeholder="https://google.com"
/>
)}
/>
</div>
<div class="w-full">
<form.AppField
name="url"
children={(field) => (
<field.TextField
name="url"
label="URL"
description={"The URL to redirect to"}
placeholder="https://google.com"
/>
)}
/>
</div>
<Button type="submit" class="my-auto">
Create
</Button>
</div>
</div>
</form>
);
};
export default CreateRedirectionForm;

View File

@@ -0,0 +1,73 @@
import { For, Match, Switch } from "solid-js";
import { listURLRedirections } from "~/gen/proto/shorten/v1/shorten-ShortenService_connectquery";
import type { URLRedirection } from "~/gen/proto/shorten/v1/shorten_pb";
import { useConnectQuery } from "~/integrations/connect-query/solid";
const ListRedirections = () => {
const query = useConnectQuery(listURLRedirections);
return (
<div class="bg-card p-4">
<h2 class="p-2 my-2">List of Redirections</h2>
<Switch fallback={<div>Loading...</div>}>
<Match when={query.isError}>
<div>Error: {query.error?.message}</div>
</Match>
<Match when={query.isLoading}>
<div>Loading...</div>
</Match>
<Match when={query.data}>
<table class="border w-full border-collapse p-2">
<thead>
<tr>
<td class="border p-2">Short Code</td>
<td class="border p-2">URL</td>
<td class="border p-2">Active</td>
<td class="border p-2">Created At</td>
<td class="border p-2">Expires At</td>
</tr>
</thead>
<tbody>
<For each={query.data?.urlRedirections}>
{(item) => <Redirection item={item} />}
</For>
</tbody>
</table>
</Match>
</Switch>
</div>
);
};
const Redirection = (props: { item: URLRedirection }) => {
return (
<tr>
<td class="border p-2">
<a
href={`https://l.kocoder.xyz/${props.item.shortCode}`}
target="_blank"
>
{props.item.shortCode}
</a>
</td>
<td class="border p-2">{props.item.url}</td>
<td class="border p-2">{props.item.isActive ? "Yes" : "No"}</td>
<td class="border p-2">
{props.item.createdAt
? new Date(
Number(props.item.createdAt.seconds) * 1000,
).toLocaleDateString()
: ""}
</td>
<td class="border p-2">
{props.item.createdAt
? new Date(
Number(props.item.expiresAt.seconds) * 1000,
).toLocaleDateString()
: ""}
</td>
</tr>
);
};
export default ListRedirections;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,28 @@
// @generated by protoc-gen-connect-query v2.2.0 with parameter "target=ts"
// @generated from file proto/shorten/v1/shorten.proto (package proto.shorten.v1, syntax proto3)
/* eslint-disable */
import { ShortenService } from "./shorten_pb";
/**
* @generated from rpc proto.shorten.v1.ShortenService.CreateURLRedirection
*/
export const createURLRedirection = ShortenService.method.createURLRedirection;
/**
* @generated from rpc proto.shorten.v1.ShortenService.ListURLRedirections
*/
export const listURLRedirections = ShortenService.method.listURLRedirections;
/**
* rpc GetURLRedirection(GetURLRedirectionRequest) returns
* (GetURLRedirectionResponse) {}
*
* @generated from rpc proto.shorten.v1.ShortenService.DeactivateURLRedirection
*/
export const deactivateURLRedirection = ShortenService.method.deactivateURLRedirection;
/**
* @generated from rpc proto.shorten.v1.ShortenService.DeleteURLRedirection
*/
export const deleteURLRedirection = ShortenService.method.deleteURLRedirection;

View File

@@ -0,0 +1,248 @@
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file proto/shorten/v1/shorten.proto (package proto.shorten.v1, syntax proto3)
/* eslint-disable */
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
import { file_buf_validate_validate } from "../../../buf/validate/validate_pb";
import type { Timestamp } from "@bufbuild/protobuf/wkt";
import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file proto/shorten/v1/shorten.proto.
*/
export const file_proto_shorten_v1_shorten: GenFile = /*@__PURE__*/
fileDesc("Ch5wcm90by9zaG9ydGVuL3YxL3Nob3J0ZW4ucHJvdG8SEHByb3RvLnNob3J0ZW4udjEiUgobQ3JlYXRlVVJMUmVkaXJlY3Rpb25SZXF1ZXN0EhwKCnNob3J0X2NvZGUYASABKAlCCLpIBXIDmAEKEhUKA3VybBgCIAEoCUIIukgFcgOIAQEiKgocQ3JlYXRlVVJMUmVkaXJlY3Rpb25SZXNwb25zZRIKCgJvaxgBIAEoCCLQAQoOVVJMUmVkaXJlY3Rpb24SDgoGdXJsX2lkGAEgASgFEhIKCnNob3J0X2NvZGUYAyABKAkSCwoDdXJsGAIgASgJEi4KCmNyZWF0ZWRfYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhEKCWlzX2FjdGl2ZRgGIAEoCBIaChJjbGlja19jb3VudF9ieV9kYXkYByADKAUiHAoaTGlzdFVSTFJlZGlyZWN0aW9uc1JlcXVlc3QiWQobTGlzdFVSTFJlZGlyZWN0aW9uc1Jlc3BvbnNlEjoKEHVybF9yZWRpcmVjdGlvbnMYASADKAsyIC5wcm90by5zaG9ydGVuLnYxLlVSTFJlZGlyZWN0aW9uIkQKH0RlYWN0aXZhdGVVUkxSZWRpcmVjdGlvblJlcXVlc3QSDgoGdXJsX2lkGAEgASgFEhEKCWlzX2FjdGl2ZRgCIAEoCCIuCiBEZWFjdGl2YXRlVVJMUmVkaXJlY3Rpb25SZXNwb25zZRIKCgJvaxgBIAEoCCItChtEZWxldGVVUkxSZWRpcmVjdGlvblJlcXVlc3QSDgoGdXJsX2lkGAEgASgFIioKHERlbGV0ZVVSTFJlZGlyZWN0aW9uUmVzcG9uc2USCgoCb2sYASABKAgy/gMKDlNob3J0ZW5TZXJ2aWNlEncKFENyZWF0ZVVSTFJlZGlyZWN0aW9uEi0ucHJvdG8uc2hvcnRlbi52MS5DcmVhdGVVUkxSZWRpcmVjdGlvblJlcXVlc3QaLi5wcm90by5zaG9ydGVuLnYxLkNyZWF0ZVVSTFJlZGlyZWN0aW9uUmVzcG9uc2UiABJ0ChNMaXN0VVJMUmVkaXJlY3Rpb25zEiwucHJvdG8uc2hvcnRlbi52MS5MaXN0VVJMUmVkaXJlY3Rpb25zUmVxdWVzdBotLnByb3RvLnNob3J0ZW4udjEuTGlzdFVSTFJlZGlyZWN0aW9uc1Jlc3BvbnNlIgASgwEKGERlYWN0aXZhdGVVUkxSZWRpcmVjdGlvbhIxLnByb3RvLnNob3J0ZW4udjEuRGVhY3RpdmF0ZVVSTFJlZGlyZWN0aW9uUmVxdWVzdBoyLnByb3RvLnNob3J0ZW4udjEuRGVhY3RpdmF0ZVVSTFJlZGlyZWN0aW9uUmVzcG9uc2UiABJ3ChREZWxldGVVUkxSZWRpcmVjdGlvbhItLnByb3RvLnNob3J0ZW4udjEuRGVsZXRlVVJMUmVkaXJlY3Rpb25SZXF1ZXN0Gi4ucHJvdG8uc2hvcnRlbi52MS5EZWxldGVVUkxSZWRpcmVjdGlvblJlc3BvbnNlIgBCyAEKFGNvbS5wcm90by5zaG9ydGVuLnYxQgxTaG9ydGVuUHJvdG9QAVpAZ2l0LmtvY29kZXIueHl6L3Z0L3Nob3J0ZW5lci9pbnRlcm5hbC9wcm90by9zaG9ydGVuL3YxO3Nob3J0ZW52MaICA1BTWKoCEFByb3RvLlNob3J0ZW4uVjHKAhBQcm90b1xTaG9ydGVuXFYx4gIcUHJvdG9cU2hvcnRlblxWMVxHUEJNZXRhZGF0YeoCElByb3RvOjpTaG9ydGVuOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_protobuf_timestamp]);
/**
* @generated from message proto.shorten.v1.CreateURLRedirectionRequest
*/
export type CreateURLRedirectionRequest = Message<"proto.shorten.v1.CreateURLRedirectionRequest"> & {
/**
* @generated from field: string short_code = 1;
*/
shortCode: string;
/**
* @generated from field: string url = 2;
*/
url: string;
};
/**
* Describes the message proto.shorten.v1.CreateURLRedirectionRequest.
* Use `create(CreateURLRedirectionRequestSchema)` to create a new message.
*/
export const CreateURLRedirectionRequestSchema: GenMessage<CreateURLRedirectionRequest> = /*@__PURE__*/
messageDesc(file_proto_shorten_v1_shorten, 0);
/**
* @generated from message proto.shorten.v1.CreateURLRedirectionResponse
*/
export type CreateURLRedirectionResponse = Message<"proto.shorten.v1.CreateURLRedirectionResponse"> & {
/**
* @generated from field: bool ok = 1;
*/
ok: boolean;
};
/**
* Describes the message proto.shorten.v1.CreateURLRedirectionResponse.
* Use `create(CreateURLRedirectionResponseSchema)` to create a new message.
*/
export const CreateURLRedirectionResponseSchema: GenMessage<CreateURLRedirectionResponse> = /*@__PURE__*/
messageDesc(file_proto_shorten_v1_shorten, 1);
/**
* @generated from message proto.shorten.v1.URLRedirection
*/
export type URLRedirection = Message<"proto.shorten.v1.URLRedirection"> & {
/**
* @generated from field: int32 url_id = 1;
*/
urlId: number;
/**
* @generated from field: string short_code = 3;
*/
shortCode: string;
/**
* @generated from field: string url = 2;
*/
url: string;
/**
* @generated from field: google.protobuf.Timestamp created_at = 4;
*/
createdAt?: Timestamp | undefined;
/**
* @generated from field: google.protobuf.Timestamp expires_at = 5;
*/
expiresAt?: Timestamp | undefined;
/**
* @generated from field: bool is_active = 6;
*/
isActive: boolean;
/**
* @generated from field: repeated int32 click_count_by_day = 7;
*/
clickCountByDay: number[];
};
/**
* Describes the message proto.shorten.v1.URLRedirection.
* Use `create(URLRedirectionSchema)` to create a new message.
*/
export const URLRedirectionSchema: GenMessage<URLRedirection> = /*@__PURE__*/
messageDesc(file_proto_shorten_v1_shorten, 2);
/**
* @generated from message proto.shorten.v1.ListURLRedirectionsRequest
*/
export type ListURLRedirectionsRequest = Message<"proto.shorten.v1.ListURLRedirectionsRequest"> & {
};
/**
* Describes the message proto.shorten.v1.ListURLRedirectionsRequest.
* Use `create(ListURLRedirectionsRequestSchema)` to create a new message.
*/
export const ListURLRedirectionsRequestSchema: GenMessage<ListURLRedirectionsRequest> = /*@__PURE__*/
messageDesc(file_proto_shorten_v1_shorten, 3);
/**
* @generated from message proto.shorten.v1.ListURLRedirectionsResponse
*/
export type ListURLRedirectionsResponse = Message<"proto.shorten.v1.ListURLRedirectionsResponse"> & {
/**
* @generated from field: repeated proto.shorten.v1.URLRedirection url_redirections = 1;
*/
urlRedirections: URLRedirection[];
};
/**
* Describes the message proto.shorten.v1.ListURLRedirectionsResponse.
* Use `create(ListURLRedirectionsResponseSchema)` to create a new message.
*/
export const ListURLRedirectionsResponseSchema: GenMessage<ListURLRedirectionsResponse> = /*@__PURE__*/
messageDesc(file_proto_shorten_v1_shorten, 4);
/**
* @generated from message proto.shorten.v1.DeactivateURLRedirectionRequest
*/
export type DeactivateURLRedirectionRequest = Message<"proto.shorten.v1.DeactivateURLRedirectionRequest"> & {
/**
* @generated from field: int32 url_id = 1;
*/
urlId: number;
/**
* @generated from field: bool is_active = 2;
*/
isActive: boolean;
};
/**
* Describes the message proto.shorten.v1.DeactivateURLRedirectionRequest.
* Use `create(DeactivateURLRedirectionRequestSchema)` to create a new message.
*/
export const DeactivateURLRedirectionRequestSchema: GenMessage<DeactivateURLRedirectionRequest> = /*@__PURE__*/
messageDesc(file_proto_shorten_v1_shorten, 5);
/**
* @generated from message proto.shorten.v1.DeactivateURLRedirectionResponse
*/
export type DeactivateURLRedirectionResponse = Message<"proto.shorten.v1.DeactivateURLRedirectionResponse"> & {
/**
* @generated from field: bool ok = 1;
*/
ok: boolean;
};
/**
* Describes the message proto.shorten.v1.DeactivateURLRedirectionResponse.
* Use `create(DeactivateURLRedirectionResponseSchema)` to create a new message.
*/
export const DeactivateURLRedirectionResponseSchema: GenMessage<DeactivateURLRedirectionResponse> = /*@__PURE__*/
messageDesc(file_proto_shorten_v1_shorten, 6);
/**
* @generated from message proto.shorten.v1.DeleteURLRedirectionRequest
*/
export type DeleteURLRedirectionRequest = Message<"proto.shorten.v1.DeleteURLRedirectionRequest"> & {
/**
* @generated from field: int32 url_id = 1;
*/
urlId: number;
};
/**
* Describes the message proto.shorten.v1.DeleteURLRedirectionRequest.
* Use `create(DeleteURLRedirectionRequestSchema)` to create a new message.
*/
export const DeleteURLRedirectionRequestSchema: GenMessage<DeleteURLRedirectionRequest> = /*@__PURE__*/
messageDesc(file_proto_shorten_v1_shorten, 7);
/**
* @generated from message proto.shorten.v1.DeleteURLRedirectionResponse
*/
export type DeleteURLRedirectionResponse = Message<"proto.shorten.v1.DeleteURLRedirectionResponse"> & {
/**
* @generated from field: bool ok = 1;
*/
ok: boolean;
};
/**
* Describes the message proto.shorten.v1.DeleteURLRedirectionResponse.
* Use `create(DeleteURLRedirectionResponseSchema)` to create a new message.
*/
export const DeleteURLRedirectionResponseSchema: GenMessage<DeleteURLRedirectionResponse> = /*@__PURE__*/
messageDesc(file_proto_shorten_v1_shorten, 8);
/**
* @generated from service proto.shorten.v1.ShortenService
*/
export const ShortenService: GenService<{
/**
* @generated from rpc proto.shorten.v1.ShortenService.CreateURLRedirection
*/
createURLRedirection: {
methodKind: "unary";
input: typeof CreateURLRedirectionRequestSchema;
output: typeof CreateURLRedirectionResponseSchema;
},
/**
* @generated from rpc proto.shorten.v1.ShortenService.ListURLRedirections
*/
listURLRedirections: {
methodKind: "unary";
input: typeof ListURLRedirectionsRequestSchema;
output: typeof ListURLRedirectionsResponseSchema;
},
/**
* rpc GetURLRedirection(GetURLRedirectionRequest) returns
* (GetURLRedirectionResponse) {}
*
* @generated from rpc proto.shorten.v1.ShortenService.DeactivateURLRedirection
*/
deactivateURLRedirection: {
methodKind: "unary";
input: typeof DeactivateURLRedirectionRequestSchema;
output: typeof DeactivateURLRedirectionResponseSchema;
},
/**
* @generated from rpc proto.shorten.v1.ShortenService.DeleteURLRedirection
*/
deleteURLRedirection: {
methodKind: "unary";
input: typeof DeleteURLRedirectionRequestSchema;
output: typeof DeleteURLRedirectionResponseSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_proto_shorten_v1_shorten, 0);

View File

@@ -0,0 +1,107 @@
import { createContext, useContext } from "solid-js";
import type { JSX } from "solid-js";
import type { Transport, ConnectError } from "@connectrpc/connect";
import { createQuery, createMutation } from "@tanstack/solid-query";
import type {
CreateQueryResult,
CreateQueryOptions,
CreateMutationResult,
CreateMutationOptions,
} from "@tanstack/solid-query";
import { createQueryOptions, callUnaryMethod } from "@connectrpc/connect-query";
import type {
DescMethodUnary,
DescMessage,
MessageInitShape,
MessageShape,
} from "@bufbuild/protobuf";
const TransportContext = createContext<Transport>();
export interface TransportProviderProps {
transport: Transport;
children: JSX.Element;
}
export function TransportProvider(props: TransportProviderProps) {
console.log("TransportProvider rendering, transport:", props.transport);
return (
<TransportContext.Provider value={props.transport}>
{props.children}
</TransportContext.Provider>
);
}
export function useTransport() {
const transport = useContext(TransportContext);
console.log("useTransport called, received:", transport);
if (!transport) {
throw new Error("useTransport must be used within a TransportProvider");
}
return transport;
}
export function useConnectQuery<I extends DescMessage, O extends DescMessage>(
schema: DescMethodUnary<I, O>,
input?: MessageInitShape<I>,
options?: Omit<
CreateQueryOptions<MessageShape<O>, ConnectError>,
"queryKey" | "queryFn"
> & {
transport?: Transport;
},
): CreateQueryResult<MessageShape<O>, ConnectError> {
const contextTransport = useTransport();
return createQuery(() => {
const transport = options?.transport ?? contextTransport;
const baseOptions = createQueryOptions(schema, input, { transport });
return {
...baseOptions,
...options,
} as any;
});
}
export function useConnectMutation<
I extends DescMessage,
O extends DescMessage,
Ctx = unknown
>(
schema: DescMethodUnary<I, O>,
options?: Omit<
CreateMutationOptions<MessageShape<O>, ConnectError, MessageInitShape<I>, Ctx>,
"mutationFn"
> & {
transport?: Transport;
},
): CreateMutationResult<MessageShape<O>, ConnectError, MessageInitShape<I>, Ctx> {
const contextTransport = useTransport();
return createMutation(() => {
const transport = options?.transport ?? contextTransport;
return {
...options,
mutationFn: async (input: MessageInitShape<I>) => {
return callUnaryMethod(transport, schema, input);
},
};
});
}
export function getConnectQueryKey<
I extends DescMessage,
O extends DescMessage
>(
schema: DescMethodUnary<I, O>,
input?: MessageInitShape<I>,
): [string, { serviceName: string; methodName: string; input?: MessageInitShape<I> }] {
return [
"connect-query",
{
serviceName: schema.parent.typeName,
methodName: schema.name,
...(input !== undefined ? { input } : {}),
},
];
}

View File

@@ -1,8 +1,14 @@
import { QueryClient } from '@tanstack/solid-query' import { QueryClient } from "@tanstack/solid-query";
import { createConnectTransport } from "@connectrpc/connect-web";
export function getContext() { export function getContext() {
const queryClient = new QueryClient() const queryClient = new QueryClient();
const transport = createConnectTransport({
baseUrl: "https://l.kocoder.xyz",
});
return { return {
queryClient, queryClient,
} transport,
};
} }

View File

@@ -10,6 +10,7 @@
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as TodoRouteImport } from './routes/todo' import { Route as TodoRouteImport } from './routes/todo'
import { Route as ShortenRouteImport } from './routes/shorten'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
const TodoRoute = TodoRouteImport.update({ const TodoRoute = TodoRouteImport.update({
@@ -17,6 +18,11 @@ const TodoRoute = TodoRouteImport.update({
path: '/todo', path: '/todo',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const ShortenRoute = ShortenRouteImport.update({
id: '/shorten',
path: '/shorten',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
@@ -25,27 +31,31 @@ const IndexRoute = IndexRouteImport.update({
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/shorten': typeof ShortenRoute
'/todo': typeof TodoRoute '/todo': typeof TodoRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/shorten': typeof ShortenRoute
'/todo': typeof TodoRoute '/todo': typeof TodoRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/shorten': typeof ShortenRoute
'/todo': typeof TodoRoute '/todo': typeof TodoRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/todo' fullPaths: '/' | '/shorten' | '/todo'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' | '/todo' to: '/' | '/shorten' | '/todo'
id: '__root__' | '/' | '/todo' id: '__root__' | '/' | '/shorten' | '/todo'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
ShortenRoute: typeof ShortenRoute
TodoRoute: typeof TodoRoute TodoRoute: typeof TodoRoute
} }
@@ -58,6 +68,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof TodoRouteImport preLoaderRoute: typeof TodoRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/shorten': {
id: '/shorten'
path: '/shorten'
fullPath: '/shorten'
preLoaderRoute: typeof ShortenRouteImport
parentRoute: typeof rootRouteImport
}
'/': { '/': {
id: '/' id: '/'
path: '/' path: '/'
@@ -70,6 +87,7 @@ declare module '@tanstack/solid-router' {
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
ShortenRoute: ShortenRoute,
TodoRoute: TodoRoute, TodoRoute: TodoRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport

View File

@@ -1,20 +1,17 @@
import { createRouter as createTanStackRouter } from "@tanstack/solid-router"; import { createRouter as createTanStackRouter } from "@tanstack/solid-router";
import { routeTree } from "./routeTree.gen"; import { routeTree } from "./routeTree.gen";
// import { getContext } from './integrations/tanstack-query/provider'
import NotFound from "./components/NotFound"; import NotFound from "./components/NotFound";
import { QueryClient } from "@tanstack/solid-query";
import { ErrorComponent } from "./routes/__root"; import { ErrorComponent } from "./routes/__root";
import { getContext } from "./integrations/tanstack-query/provider"; import { getContext } from "./integrations/tanstack-query/provider";
import { setupRouterSsrQueryIntegration } from "@tanstack/solid-router-ssr-query"; import { setupRouterSsrQueryIntegration } from "@tanstack/solid-router-ssr-query";
const queryClient = new QueryClient();
export function getRouter() { export function getRouter() {
const context = getContext();
const router = createTanStackRouter({ const router = createTanStackRouter({
routeTree, routeTree,
context: getContext(), context,
scrollRestoration: true, scrollRestoration: true,
defaultPreload: "intent", defaultPreload: "intent",
@@ -26,7 +23,7 @@ export function getRouter() {
setupRouterSsrQueryIntegration({ setupRouterSsrQueryIntegration({
router, router,
queryClient, queryClient: context.queryClient,
}); });
return router; return router;

View File

@@ -6,6 +6,8 @@ import {
} from "@tanstack/solid-router"; } from "@tanstack/solid-router";
import { TanStackRouterDevtools } from "@tanstack/solid-router-devtools"; import { TanStackRouterDevtools } from "@tanstack/solid-router-devtools";
import { QueryClientProvider, QueryClient } from "@tanstack/solid-query"; import { QueryClientProvider, QueryClient } from "@tanstack/solid-query";
import type { Transport } from "@connectrpc/connect";
import { TransportProvider } from "~/integrations/connect-query/solid";
import "@fontsource/inter/400.css"; import "@fontsource/inter/400.css";
@@ -18,6 +20,7 @@ import { ThemeProvider } from "~/components/theme-provider";
export interface MyRouterContext { export interface MyRouterContext {
queryClient: QueryClient; queryClient: QueryClient;
transport: Transport;
} }
export function ErrorComponent({ error }: { error: Error }) { export function ErrorComponent({ error }: { error: Error }) {
@@ -35,6 +38,7 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
function RootComponent() { function RootComponent() {
const context = Route.useRouteContext(); const context = Route.useRouteContext();
console.log("Root context:", context());
return ( return (
<html> <html>
<head> <head>
@@ -42,16 +46,18 @@ function RootComponent() {
<HeadContent /> <HeadContent />
</head> </head>
<body> <body>
<QueryClientProvider client={context().queryClient}> <TransportProvider transport={context().transport}>
<Suspense> <QueryClientProvider client={context().queryClient}>
<ThemeProvider> <Suspense>
<Header /> <ThemeProvider>
<Outlet /> <Header />
</ThemeProvider> <Outlet />
</ThemeProvider>
<TanStackRouterDevtools /> <TanStackRouterDevtools />
</Suspense> </Suspense>
</QueryClientProvider> </QueryClientProvider>
</TransportProvider>
<Scripts /> <Scripts />
</body> </body>
</html> </html>

17
src/routes/shorten.tsx Normal file
View File

@@ -0,0 +1,17 @@
import { createFileRoute } from "@tanstack/solid-router";
import ListRedirections from "~/components/shortener/listRedirections";
import CreateRedirectionForm from "~/components/shortener/createRedirectionForm";
export const Route = createFileRoute("/shorten")({
component: RouteComponent,
});
function RouteComponent() {
return (
<div class="p-4 flex flex-col gap-y-4">
<h1 class="text-xl text-bold">URL Redirections</h1>
<CreateRedirectionForm />
<ListRedirections />
</div>
);
}

View File

@@ -5,6 +5,14 @@ globalThis.Response = FastResponse;
export default createServerEntry({ export default createServerEntry({
fetch(request) { fetch(request) {
return handler.fetch(request) console.log("==> Server entry fetch start:", request.method, request.url);
try {
const res = handler.fetch(request);
console.log("<== Server entry fetch sync return:", res);
return res;
} catch (e) {
console.error("<== Server entry fetch sync throw:", e);
throw e;
}
}, },
}) })