change: everything
This commit is contained in:
parent
1e09c02cb9
commit
a49c5d8bcc
15 changed files with 1107 additions and 229 deletions
|
|
@ -23,36 +23,36 @@ export interface PostProps {
|
||||||
postId: number;
|
postId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function Post({ postId }: PostProps) {
|
export default function Post({ postId }: PostProps) {
|
||||||
const [postContent, setPostContent] = useState<PostContent>();
|
const [postContent, setPostContent] = useState<PostContent>();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPost = async () => {
|
const fetchPost = async () => {
|
||||||
const response = await fetch(`http://localhost:8787/posts/${postId}/view`);
|
try {
|
||||||
const jsonContent: Array<PostContent> = await response.json();
|
// CHANGED: Fetch from your internal resource route
|
||||||
|
// This triggers the 'loader' we created in step 2
|
||||||
|
const response = await fetch(`/api/posts/${postId}`);
|
||||||
|
|
||||||
let newPostContent: PostContent = jsonContent[0] as PostContent;
|
if (!response.ok) throw new Error("Failed to fetch");
|
||||||
|
|
||||||
if (newPostContent) {
|
const jsonContent: Array<PostContent> = await response.json();
|
||||||
setPostContent(newPostContent);
|
let newPostContent: PostContent = jsonContent[0] as PostContent;
|
||||||
|
|
||||||
|
if (newPostContent) {
|
||||||
|
setPostContent(newPostContent);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching post:", error);
|
||||||
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchPost();
|
fetchPost();
|
||||||
}, []);
|
}, [postId]); // Added postId to dependency array
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) return <div>Loading...</div>;
|
||||||
return <div>Loading...</div>;
|
if (!postContent) return <p>Failed to load!</p>;
|
||||||
}
|
|
||||||
|
|
||||||
if (!postContent) {
|
return <StaticPost postContent={postContent} />;
|
||||||
return <p>Failed to load!</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StaticPost postContent={postContent} />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
@ -30,7 +30,12 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<Meta />
|
<title>anum</title>
|
||||||
|
<meta property="og:title" content="anum" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:description" content="a modern forum made in the 90s' image" />
|
||||||
|
<meta property="og:locale" content="en_US" />
|
||||||
|
<meta property="og.locale:alternate" content="en_GB" />
|
||||||
<Links />
|
<Links />
|
||||||
</head>
|
</head>
|
||||||
<body className="h-full">
|
<body className="h-full">
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,6 @@ export default [
|
||||||
index("routes/home.tsx"),
|
index("routes/home.tsx"),
|
||||||
route("login", "routes/login.tsx"),
|
route("login", "routes/login.tsx"),
|
||||||
route("create", "routes/create.tsx"),
|
route("create", "routes/create.tsx"),
|
||||||
route("view", "routes/view.tsx")
|
route("view", "routes/view.tsx"),
|
||||||
|
route("api/posts/:postId", "routes/api.posts.$postId.ts")
|
||||||
] satisfies RouteConfig;
|
] satisfies RouteConfig;
|
||||||
|
|
|
||||||
16
app/routes/api.posts.$postId.ts
Normal file
16
app/routes/api.posts.$postId.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { type LoaderFunctionArgs } from "react-router";
|
||||||
|
|
||||||
|
export async function loader({ params, context }: LoaderFunctionArgs) {
|
||||||
|
const { postId } = params;
|
||||||
|
|
||||||
|
// 'context.cloudflare.env' contains your bindings
|
||||||
|
const { API } = context.cloudflare.env;
|
||||||
|
|
||||||
|
// Use the Service Binding to talk to the backend
|
||||||
|
// This is much faster than a standard fetch over the internet
|
||||||
|
const response = await API.fetch(
|
||||||
|
`http://internal/posts/${postId}/view`
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
@ -1,92 +1,90 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { Form, useActionData, useNavigate } from "react-router";
|
||||||
import { useLocation } from "react-router";
|
import { useLocation } from "react-router";
|
||||||
|
import { redirect, type ActionFunctionArgs } from "react-router";
|
||||||
|
|
||||||
|
|
||||||
|
// server-side action so we don't have to use clearnet for front-backend comms
|
||||||
|
// praise cloudflare
|
||||||
|
export async function action({ request, context }: ActionFunctionArgs) {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const env = context.cloudflare.env as { API: Fetcher };
|
||||||
|
|
||||||
|
const postTitle = formData.get("title") as string;
|
||||||
|
const postContent = formData.get("content") as string;
|
||||||
|
const parentId = formData.get("parentId") as string;
|
||||||
|
|
||||||
|
// internal url uses ternary operator
|
||||||
|
const path = parentId
|
||||||
|
? `/posts/${parentId}/reply`
|
||||||
|
: "/posts/add";
|
||||||
|
|
||||||
|
const response = await env.API.fetch(new Request(`http://internal${path}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
// forward cookies or auth won't work
|
||||||
|
"Cookie": request.headers.get("Cookie") || "",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: postTitle,
|
||||||
|
content: postContent,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
return { error: `Failed to create post: ${response.status} - ${error}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect("/");
|
||||||
|
}
|
||||||
|
|
||||||
export default function CreatePostPage() {
|
export default function CreatePostPage() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const stateParentId = location.state?.parentId;
|
const stateParentId = location.state?.parentId;
|
||||||
const [postTitle, setPostTitle] = useState("");
|
|
||||||
const [postContent, setPostContent] = useState("");
|
|
||||||
const [parentId, setParentId] = useState(stateParentId || "");
|
const [parentId, setParentId] = useState(stateParentId || "");
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
console.log(stateParentId);
|
console.log(stateParentId);
|
||||||
console.log(parentId);
|
console.log(parentId);
|
||||||
|
|
||||||
async function createPost() {
|
|
||||||
if (parentId) {
|
|
||||||
const response = await fetch(`http://localhost:8787/posts/${parentId}/reply`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
title: postTitle,
|
|
||||||
content: postContent // god bless HTTPS
|
|
||||||
}),
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Got ${response.status} when trying to log in.`)
|
|
||||||
}
|
|
||||||
navigate("/");
|
|
||||||
} else {
|
|
||||||
const response = await fetch("http://localhost:8787/posts/add", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
title: postTitle,
|
|
||||||
content: postContent // god bless HTTPS
|
|
||||||
}),
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Got ${response.status} when trying to log in.`)
|
|
||||||
}
|
|
||||||
navigate("/");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[60vw] h-[50vh] flex flex-col absolute top-1/2 left-1/2 p-8 -translate-x-1/2 -translate-y-1/2 shadow-black/40 shadow-2xl rounded-2xl bg-[#171717] text-[#fafafa] transition ease-in-out duration-300">
|
<div className="w-[92vw] md:w-[60vw] min-h-[60vh] md:min-h-0 md:h-[50vh] flex flex-col absolute top-1/2 left-1/2 p-4 md:p-8 -translate-x-1/2 -translate-y-1/2 shadow-black/40 shadow-2xl rounded-2xl bg-[#171717] text-[#fafafa] transition ease-in-out duration-300">
|
||||||
<h1 className="text-2xl font-black text-center mb-4">Create Post</h1>
|
<h1 className="text-xl md:text-2xl font-black text-center mb-4">Create Post</h1>
|
||||||
|
|
||||||
<form className="flex-1 flex flex-col gap-4" action={createPost}>
|
<Form className="flex-1 flex flex-col gap-4" method="post">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex flex-col md:flex-row gap-2 md:items-center">
|
||||||
<label htmlFor="title" className="px-1 font-semibold">Post Title</label>
|
<label htmlFor="title" className="px-1 font-semibold">Post Title</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="title"
|
name="title"
|
||||||
className="bg-transparent border-[1px] border-white/40 rounded-xl p-2 focus:border-white outline-none flex-1 hover:border-white/60 transition ease-in-out duration-200"
|
className="bg-transparent border-[1px] border-white/40 rounded-xl p-2 focus:border-white outline-none flex-1 hover:border-white/60 transition ease-in-out duration-200"
|
||||||
onChange={(e) => setPostTitle(e.target.value)}
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col gap-2">
|
<div className="flex-1 flex flex-col gap-2">
|
||||||
<label htmlFor="content" className="px-1 font-semibold">Post Contents</label>
|
<label htmlFor="content" className="px-1 font-semibold">Post Contents</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="content"
|
name="content"
|
||||||
className="flex-1 bg-transparent border-[1px] border-white/40 rounded-xl p-3 focus:border-white outline-none resize-none hover:border-white/60 transition ease-in-out duration-200"
|
className="flex-1 min-h-[120px] md:min-h-0 bg-transparent border-[1px] border-white/40 rounded-xl p-3 focus:border-white outline-none resize-none hover:border-white/60 transition ease-in-out duration-200"
|
||||||
onChange={(e) => setPostContent(e.target.value)}
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex flex-col md:flex-row gap-2 md:items-center">
|
||||||
<label htmlFor="title" className="px-1 font-semibold">Parent Post ID (leave empty if none)</label>
|
<label htmlFor="parentId" className="px-1 font-semibold">Parent Post ID (leave empty if none)</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="title"
|
name="parentId"
|
||||||
className="bg-transparent border-[1px] border-white/40 rounded-xl p-2 focus:border-white outline-none hover:border-white/60 transition ease-in-out duration-200"
|
className="bg-transparent border-[1px] border-white/40 rounded-xl p-2 focus:border-white outline-none hover:border-white/60 transition ease-in-out duration-200"
|
||||||
value={parentId}
|
value={parentId}
|
||||||
onChange={(e) => setParentId(e.target.value)}
|
onChange={(e) => setParentId(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button className="bg-white text-black font-bold rounded-xl p-2 ml-auto transition ease-in-out duration-300 hover:scale-125" role="submit">Create Post</button>
|
<button className="bg-white text-black font-bold rounded-xl p-2 md:ml-auto w-full md:w-auto transition ease-in-out duration-300 hover:scale-125" type="submit">Create Post</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,35 @@
|
||||||
import type { Route } from "./+types/home";
|
import { useLoaderData } from "react-router";
|
||||||
import StaticPost from "../components/staticPost";
|
import StaticPost from "../components/staticPost";
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import type { PostContent } from "../components/post";
|
import type { PostContent } from "../components/post";
|
||||||
|
|
||||||
export function meta({ }: Route.MetaArgs) {
|
// Server-side loader - runs in Cloudflare Worker
|
||||||
return [
|
export async function loader({ context }: { context: { cloudflare: { env: { API: Fetcher } } } }) {
|
||||||
{ title: "New React Router App" },
|
const env = context.cloudflare.env;
|
||||||
{ name: "description", content: "Welcome to React Router!" },
|
|
||||||
];
|
// Use service binding instead of fetch()
|
||||||
}
|
const response = await env.API.fetch(new Request("http://internal/feed"));
|
||||||
|
|
||||||
export function loader({ context }: Route.LoaderArgs) {
|
if (!response.ok) {
|
||||||
return { message: context.cloudflare.env.VALUE_FROM_CLOUDFLARE };
|
throw new Response("Failed to load feed", { status: response.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedContent: PostContent[] = await response.json();
|
||||||
|
return { feedContent };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Client component - no useEffect needed!
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [feedContent, setFeedContent] = useState<Array<PostContent>>();
|
const { feedContent } = useLoaderData<typeof loader>();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (!feedContent || feedContent.length === 0) {
|
||||||
const fetchFeed = async () => {
|
return <p>No posts found.</p>;
|
||||||
try {
|
|
||||||
const response = await fetch(`http://localhost:8787/feed`);
|
|
||||||
|
|
||||||
// check if response is actually OK (status 200-299)
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonContent: PostContent[] = await response.json();
|
|
||||||
setFeedContent(jsonContent);
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Fetch failed:", e);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchFeed();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <div>Loading...</div>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!feedContent) {
|
|
||||||
return <p>Failed to load!</p>;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{feedContent && feedContent.length > 0 ? (
|
{feedContent.map((post) => (
|
||||||
feedContent.map((post) => {
|
<StaticPost key={post.id} postContent={post} />
|
||||||
return <StaticPost postContent={post} />
|
))}
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<p>No replies found for this thread.</p>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,92 +1,146 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { Form, redirect, useActionData, type ActionFunctionArgs } from "react-router";
|
||||||
|
|
||||||
|
export async function action({ request, context }: ActionFunctionArgs) {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const env = context.cloudflare.env as { API: Fetcher };
|
||||||
|
|
||||||
|
const mode = formData.get("mode"); // 'login' or 'register'
|
||||||
|
const email = formData.get("email");
|
||||||
|
const password = formData.get("password");
|
||||||
|
const name = formData.get("username");
|
||||||
|
const display_name = formData.get("displayName");
|
||||||
|
|
||||||
|
// Determine path based on the radio selection
|
||||||
|
const path = mode === "login" ? "/users/login" : "/users/create";
|
||||||
|
|
||||||
|
// Construct body based on mode
|
||||||
|
const body = mode === "login"
|
||||||
|
? { email, password }
|
||||||
|
: { email, password, name, display_name };
|
||||||
|
|
||||||
|
const response = await env.API.fetch(new Request(`http://internal${path}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cookie": request.headers.get("Cookie") || "",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
return { error: `Error (${response.status}): ${errorText}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Important: We proxy the 'Set-Cookie' header from the internal API
|
||||||
|
// back to the browser so the user actually gets logged in.
|
||||||
|
const headers = new Headers();
|
||||||
|
const setCookie = response.headers.get("Set-Cookie");
|
||||||
|
if (setCookie) {
|
||||||
|
headers.append("Set-Cookie", setCookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect("/", { headers });
|
||||||
|
}
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [loginRegisterSelection, setLoginRegisterSelection] = useState("option1");
|
const [loginRegisterSelection, setLoginRegisterSelection] = useState("login");
|
||||||
const [userEmail, setUserEmail] = useState("");
|
const actionData = useActionData() as { error?: string } | undefined;
|
||||||
const [userPassword, setUserPassword] = useState("");
|
|
||||||
const [userName, setUserName] = useState("");
|
|
||||||
const [userDisplayName, setUserDisplayName] = useState("");
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
async function loginOrRegister() {
|
|
||||||
if (loginRegisterSelection === "login") {
|
|
||||||
const response = await fetch("http://localhost:8787/users/login", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: userEmail,
|
|
||||||
password: userPassword // god bless HTTPS
|
|
||||||
}),
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Got ${response.status} when trying to log in.`)
|
|
||||||
}
|
|
||||||
navigate("/");
|
|
||||||
} else {
|
|
||||||
const response = await fetch("http://localhost:8787/users/create", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: userEmail,
|
|
||||||
password: userPassword,
|
|
||||||
name: userName,
|
|
||||||
display_name: userDisplayName
|
|
||||||
}),
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Got ${response.status} when trying to register.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate("/");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 p-5 shadow-none shadow-black/0 hover:shadow-black/40 hover:shadow-2xl rounded-2xl bg-[#151515] text-center transition ease-in-out duration-300 hover:ring-2 hover:ring-[#cccccc]">
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 p-5 shadow-none shadow-black/0 hover:shadow-black/40 hover:shadow-2xl rounded-2xl bg-[#151515] text-center transition ease-in-out duration-300 hover:ring-2 hover:ring-[#cccccc]">
|
||||||
<h1 className="text-2xl font-black">Login/Register</h1>
|
<h1 className="text-2xl font-black mb-4">Login/Register</h1>
|
||||||
<form action={loginOrRegister}>
|
|
||||||
<div className="flex gap-4 justify-center">
|
{actionData?.error && (
|
||||||
|
<div className="bg-red-500/20 border border-red-500 text-red-200 p-2 rounded-xl mb-4 text-sm">
|
||||||
|
{actionData.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form method="post">
|
||||||
|
{/* We use a hidden input or the radio group to tell the action which mode we are in */}
|
||||||
|
<div className="flex gap-4 justify-center mb-4">
|
||||||
<div>
|
<div>
|
||||||
<input type="radio" id="loginOption" name="loginRegisterSelection" value="Login" checked={loginRegisterSelection === 'login'} onChange={() => setLoginRegisterSelection('login')} />
|
<input
|
||||||
<label htmlFor="loginOption">Login</label>
|
type="radio"
|
||||||
|
id="loginOption"
|
||||||
|
name="mode"
|
||||||
|
value="login"
|
||||||
|
checked={loginRegisterSelection === 'login'}
|
||||||
|
onChange={() => setLoginRegisterSelection('login')}
|
||||||
|
/>
|
||||||
|
<label htmlFor="loginOption" className="ml-2">Login</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input type="radio" id="registerOption" name="loginRegisterSelection" value="Register" checked={loginRegisterSelection === 'register'} onChange={() => setLoginRegisterSelection('register')} />
|
<input
|
||||||
<label htmlFor="registerOption">Register</label>
|
type="radio"
|
||||||
|
id="registerOption"
|
||||||
|
name="mode"
|
||||||
|
value="register"
|
||||||
|
checked={loginRegisterSelection === 'register'}
|
||||||
|
onChange={() => setLoginRegisterSelection('register')}
|
||||||
|
/>
|
||||||
|
<label htmlFor="registerOption" className="ml-2">Register</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex m-2 my-3">
|
<div className="flex flex-col gap-3">
|
||||||
<label htmlFor="email" className="mx-4">Email</label>
|
<div className="flex items-center justify-between">
|
||||||
<input type="text" id="email" className="border-[1px] border-white rounded-xl text-center" onChange={(e) => setUserEmail(e.target.value)} />
|
<label htmlFor="email" className="mx-4">Email</label>
|
||||||
</div>
|
<input
|
||||||
<div className="flex m-2 my-3">
|
name="email"
|
||||||
<label htmlFor="password" className="mx-4">Password</label>
|
type="email"
|
||||||
<input type="text" id="password" className="border-[1px] border-white rounded-xl text-center" onChange={(e) => setUserPassword(e.target.value)} />
|
id="email"
|
||||||
</div>
|
required
|
||||||
{loginRegisterSelection === 'register' &&
|
className="bg-transparent border-[1px] border-white/40 rounded-xl text-center p-1 focus:border-white outline-none"
|
||||||
<>
|
/>
|
||||||
<div className="flex m-2 my-3">
|
</div>
|
||||||
<label htmlFor="username" className="mx-4">Username</label>
|
|
||||||
<input type="text" id="username" className="border-[1px] border-white rounded-xl text-center" onChange={(e) => setUserName(e.target.value)} />
|
|
||||||
</div>
|
|
||||||
<div className="flex m-2 my-3">
|
|
||||||
<label htmlFor="displayName" className="mx-4">Display name</label>
|
|
||||||
<input type="text" id="displayName" className="border-[1px] border-white rounded-xl text-center" onChange={(e) => setUserDisplayName(e.target.value)} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
<button className="bg-white text-black font-bold rounded-xl p-2" role="submit">login/register</button>
|
<div className="flex items-center justify-between">
|
||||||
</form>
|
<label htmlFor="password" className="mx-4">Password</label>
|
||||||
|
<input
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
required
|
||||||
|
className="bg-transparent border-[1px] border-white/40 rounded-xl text-center p-1 focus:border-white outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loginRegisterSelection === 'register' && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label htmlFor="username" className="mx-4">Username</label>
|
||||||
|
<input
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
required
|
||||||
|
className="bg-transparent border-[1px] border-white/40 rounded-xl text-center p-1 focus:border-white outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label htmlFor="displayName" className="mx-4">Display Name</label>
|
||||||
|
<input
|
||||||
|
name="displayName"
|
||||||
|
type="text"
|
||||||
|
id="displayName"
|
||||||
|
required
|
||||||
|
className="bg-transparent border-[1px] border-white/40 rounded-xl text-center p-1 focus:border-white outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="mt-6 bg-white text-black font-bold rounded-xl p-2 px-6 hover:scale-105 transition-transform"
|
||||||
|
>
|
||||||
|
{loginRegisterSelection === 'login' ? 'Login' : 'Register'}
|
||||||
|
</button>
|
||||||
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import Post from '../components/post.tsx';
|
import Post from '../components/post';
|
||||||
import { useLocation } from "react-router";
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
export default function ViewPost() {
|
export default function ViewPost() {
|
||||||
|
|
|
||||||
26
app/util/mobile-opt.tsx
Normal file
26
app/util/mobile-opt.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
const isMobile = () => {
|
||||||
|
const info =
|
||||||
|
navigator.userAgent ||
|
||||||
|
navigator.vendor ||
|
||||||
|
(window as Window & { opera?: string }).opera ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
return (
|
||||||
|
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(
|
||||||
|
info,
|
||||||
|
) ||
|
||||||
|
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(
|
||||||
|
info.substr(0, 4),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DesktopBrowser: React.FC<Props> = ({ children }) =>
|
||||||
|
!isMobile() && children;
|
||||||
|
|
||||||
|
export const MobileBrowser: React.FC<Props> = ({ children }) =>
|
||||||
|
isMobile() && children;
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
"typecheck": "npm run cf-typegen && react-router typegen && tsc -b"
|
"typecheck": "npm run cf-typegen && react-router typegen && tsc -b"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tanstack/react-start": "^1.159.0",
|
||||||
"isbot": "^5.1.31",
|
"isbot": "^5.1.31",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
|
|
||||||
837
pnpm-lock.yaml
generated
837
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
|
|
@ -17,7 +17,6 @@
|
||||||
"module": "ES2022",
|
"module": "ES2022",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"baseUrl": ".",
|
|
||||||
"rootDirs": [".", "./.react-router/types"],
|
"rootDirs": [".", "./.react-router/types"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./app/*"]
|
"~/*": ["./app/*"]
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@ import { cloudflare } from "@cloudflare/vite-plugin";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
import { cloudflareDevProxy } from "@react-router/dev/vite/cloudflare";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
cloudflare({ viteEnvironment: { name: "ssr" } }),
|
cloudflare({ viteEnvironment: { name: "ssr" } }),
|
||||||
|
cloudflareDevProxy(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
reactRouter(),
|
reactRouter(),
|
||||||
tsconfigPaths(),
|
tsconfigPaths(),
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,9 @@
|
||||||
"name": "anum-frontend",
|
"name": "anum-frontend",
|
||||||
"compatibility_date": "2025-04-04",
|
"compatibility_date": "2025-04-04",
|
||||||
"main": "./workers/app.ts",
|
"main": "./workers/app.ts",
|
||||||
"vars": {
|
|
||||||
"VALUE_FROM_CLOUDFLARE": "Hello from Cloudflare"
|
|
||||||
},
|
|
||||||
"observability": {
|
"observability": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}
|
},
|
||||||
/**
|
/**
|
||||||
* Smart Placement
|
* Smart Placement
|
||||||
* https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
* https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
||||||
|
|
@ -34,4 +31,10 @@
|
||||||
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
|
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
|
||||||
*/
|
*/
|
||||||
// "services": [ { "binding": "MY_SERVICE", "service": "my-service" } ]
|
// "services": [ { "binding": "MY_SERVICE", "service": "my-service" } ]
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "API",
|
||||||
|
"service": "anum-backend",
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue