Skip to content

Commit

Permalink
Merge pull request #5 from azu/migrate-app-router
Browse files Browse the repository at this point in the history
recactor(web): migrate to App Router
  • Loading branch information
azu authored Feb 24, 2024
2 parents 3235481 + 34f1aa6 commit 5e4bd84
Show file tree
Hide file tree
Showing 23 changed files with 2,143 additions and 1,015 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ cp .env.example .env
```
S3_AWS_ACCESS_KEY_ID="x"
S3_AWS_SECRET_ACCESS_KEY="x"
S3_AWS_REGION="us-east-1"
S3_BUCKET_NAME="x"
TWITTER_APP_KEY="YOUR_TWITTER_API_KEY"
TWITTER_APP_SECRET="YOUR_TWITTER_API_KEY_SECRET"
Expand Down Expand Up @@ -88,6 +89,7 @@ Example Permission policies:
```shell
S3_AWS_ACCESS_KEY_ID="x"
S3_AWS_SECRET_ACCESS_KEY="x"
S3_AWS_REGION="us-east-1"
S3_BUCKET_NAME="x"
TWITTER_APP_KEY="YOUR_TWITTER_API_KEY"
TWITTER_APP_SECRET="YOUR_TWITTER_API_KEY_SECRET"
Expand All @@ -104,6 +106,7 @@ If you want to get [Bluesky](https://bsky.app/) posts, you can use `yarn run fet
```ts
S3_AWS_ACCESS_KEY_ID="x"
S3_AWS_SECRET_ACCESS_KEY="x"
S3_AWS_REGION="us-east-1"
S3_BUCKET_NAME="x"
BLUESKY_IDENTIFIER="xxx.bsky.social"
BLUESKY_APPPASSWORD="x"
Expand Down Expand Up @@ -192,6 +195,7 @@ This template repository includes [.github/workflows/update.yml](.github/workflo
2. Put following env to Action's secrets
- `S3_AWS_ACCESS_KEY_ID`
- `S3_AWS_SECRET_ACCESS_KEY`
- `S3_AWS_REGION`
- `S3_BUCKET_NAME`
- If you want to fetch tweets
- `TWITTER_APP_KEY`
Expand Down
36 changes: 36 additions & 0 deletions web/app/client/CompositionInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";
import { CSSProperties, useCallback, useState } from "react";

export function CompositionInput(props: { style?: CSSProperties; value: string; onInput: (value: string) => void }) {
const [inputValue, setInputValue] = useState(props.value);
const [isComposing, setIsComposing] = useState(false);
const onInput = useCallback(
(event) => {
const value = event.currentTarget.value;
setInputValue(value);
if (!isComposing) {
props.onInput(value);
}
},
[isComposing]
);
const onCompositionStart = useCallback((e) => {
setIsComposing(true);
}, []);
const onCompositionEnd = useCallback((event) => {
setIsComposing(false);
const value = event.currentTarget.value;
setInputValue(value);
props.onInput(value);
}, []);
return (
<input
type={"text"}
value={inputValue}
onInput={onInput}
style={props.style}
onCompositionStart={onCompositionStart}
onCompositionEnd={onCompositionEnd}
/>
);
}
58 changes: 58 additions & 0 deletions web/app/client/SearchBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use client";
import { useDeferredValue, useEffect, useMemo, useState, useTransition } from "react";
import { CompositionInput } from "./CompositionInput";
import { useTypeUrlSearchParams } from "../lib/useTypeUrlSearchParams";
import { HomPageSearchParam } from "../page";
import { useTransitionContext } from "./TransitionContext";

const debounce = (fn: (..._: any[]) => void, delay: number) => {
let timeout: any;
return (...args: any[]) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
fn(...args);
}, delay);
};
};
const useSearch = ({ query }: { query?: string }) => {
const [isSearching, startTransition] = useTransition();
const [inputQuery, setInputQuery] = useState(query ?? "");
const deferredInputValue = useDeferredValue(inputQuery);
const searchParams = useTypeUrlSearchParams<HomPageSearchParam>();
const { setIsLoadingTimeline } = useTransitionContext();
useEffect(() => {
setIsLoadingTimeline(isSearching);
}, [isSearching]);
useEffect(() => {
if (deferredInputValue === query) return;
startTransition(() => {
searchParams.pushParams({
q: deferredInputValue
});
});
}, [deferredInputValue]);
const handlers = useMemo(
() => ({
search: debounce((query: string) => {
setInputQuery(query);
}, 300)
}),
[]
);
return {
inputQuery,
isSearching,
handlers
};
};
export const SearchBox = (props: { query?: string }) => {
const { inputQuery, handlers } = useSearch({ query: props.query });
return (
<div>
<label>
Search:
<CompositionInput value={inputQuery} onInput={handlers.search} style={{ width: "100%" }} />
</label>
</div>
);
};
50 changes: 50 additions & 0 deletions web/app/client/SearchMore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use client";
import { LineTweetResponse } from "../server/search";
import { useMemo, useTransition } from "react";
import { useTypeUrlSearchParams } from "../lib/useTypeUrlSearchParams";
import { HomPageSearchParam } from "../page";

export const useSearchMore = (props: { searchResults: LineTweetResponse[] }) => {
const searchParams = useTypeUrlSearchParams<HomPageSearchParam>();
const [isLoadingMore, startTransition] = useTransition();
const handlers = useMemo(() => {
return {
handleMoreTweets: () => {
const lastItemTimeStamp = props.searchResults[props.searchResults.length - 1].timestamp;
if (!lastItemTimeStamp) {
return;
}
startTransition(() => {
searchParams.pushParams({
timestamp: String(lastItemTimeStamp)
});
});
}
};
}, [props.searchResults]);
return {
handlers,
isLoadingMore
} as const;
};
export const SearchMore = (props: { searchResults: LineTweetResponse[] }) => {
const { handlers, isLoadingMore } = useSearchMore(props);
return (
<div
style={{
display: "flex",
flexDirection: "column"
}}
>
<button
onClick={handlers.handleMoreTweets}
disabled={isLoadingMore}
style={{
opacity: isLoadingMore ? 0.5 : 1
}}
>
More Tweets
</button>
</div>
);
};
19 changes: 19 additions & 0 deletions web/app/client/SearchResultContentWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client";
import { useTransitionContext } from "./TransitionContext";

export const SearchResultContentWrapper = (props: { children: React.ReactNode }) => {
const { isLoadingTimeline } = useTransitionContext();
return (
<div
style={
isLoadingTimeline
? {
opacity: 0.5
}
: {}
}
>
{props.children}
</div>
);
};
26 changes: 26 additions & 0 deletions web/app/client/TransitionContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";
import { createContext, ReactNode, useContext, useState } from "react";

export type TransitionContext = {
isLoadingTimeline: boolean;
setIsLoadingTimeline: (isLoading: boolean) => void;
};
const TransitionContext = createContext<TransitionContext>({
isLoadingTimeline: false,
setIsLoadingTimeline: () => {}
});
export const TransitionContextProvider = (props: { children: ReactNode }) => {
const [isLoadingTimeline, setIsLoadingTimeline] = useState(false);
return (
<TransitionContext.Provider value={{ isLoadingTimeline, setIsLoadingTimeline }}>
{props.children}
</TransitionContext.Provider>
);
};
export const useTransitionContext = () => {
const context = useContext(TransitionContext);
if (!context) {
throw new Error("useTransitionContext must be used within a TransitionContextProvider");
}
return context;
};
Loading

0 comments on commit 5e4bd84

Please sign in to comment.