Skip to content

Commit b95fc98

Browse files
committed
refactor(home): remove DesktopView and MobileView components, integrate HomePage for unified layout
1 parent 85285e3 commit b95fc98

5 files changed

Lines changed: 262 additions & 292 deletions

File tree

packages/webapp/src/components/pages/home/DesktopView.tsx

Lines changed: 0 additions & 85 deletions
This file was deleted.
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import { useState, useRef, useCallback, KeyboardEvent, ChangeEvent } from 'react'
2+
import { useRouter } from 'next/router'
3+
import slugify from 'slugify'
4+
import dynamic from 'next/dynamic'
5+
import HeadSeo from '@components/HeadSeo'
6+
import { DocsPlus } from '@components/icons/Icons'
7+
import { Avatar } from '@components/ui/Avatar'
8+
import Button from '@components/ui/Button'
9+
import Input from '@components/ui/Input'
10+
import Loading from '@components/ui/Loading'
11+
import Modal from '@components/ui/Modal'
12+
import TabLayout from '../TabLayout'
13+
import { useAuthStore, useStore } from '@stores'
14+
import { LuGithub, LuMessageCircle } from 'react-icons/lu'
15+
16+
const SignInPanel = dynamic(() => import('@pages/panels/SignInPanel'), {
17+
loading: () => <Loading />
18+
})
19+
const ProfilePanel = dynamic(() => import('@pages/panels/profile/ProfilePanel'), {
20+
loading: () => <Loading />
21+
})
22+
23+
interface HomePageProps {
24+
hostname: string
25+
}
26+
27+
const HomePage = ({ hostname }: HomePageProps) => {
28+
const router = useRouter()
29+
const user = useAuthStore((state) => state.profile)
30+
const { isAuthServiceAvailable } = useStore((state) => state.settings)
31+
32+
const [documentName, setDocumentName] = useState('')
33+
const [isLoading, setIsLoading] = useState(false)
34+
const [isProfileOpen, setIsProfileOpen] = useState(false)
35+
const [isSignInOpen, setIsSignInOpen] = useState(false)
36+
const inputRef = useRef<HTMLInputElement>(null)
37+
38+
const navigateToDocument = useCallback(
39+
(name?: string) => {
40+
setIsLoading(true)
41+
let docSlug = name || documentName
42+
43+
if (!docSlug) {
44+
// Generate random slug
45+
docSlug = (Math.random() + 1).toString(36).substring(2)
46+
}
47+
48+
// Sanitize the slug
49+
let sanitizedSlug = slugify(docSlug, { lower: true, strict: true })
50+
51+
if (sanitizedSlug.length < 3) {
52+
sanitizedSlug = sanitizedSlug.padEnd(3, 'x')
53+
} else if (sanitizedSlug.length > 30) {
54+
sanitizedSlug = sanitizedSlug.substring(0, 30)
55+
}
56+
57+
router.push(`/${sanitizedSlug}`)
58+
},
59+
[documentName, router]
60+
)
61+
62+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
63+
if (e.key === 'Enter' && documentName) {
64+
navigateToDocument()
65+
}
66+
}
67+
68+
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
69+
setDocumentName(e.target.value)
70+
}
71+
72+
// Scroll input into view when focused (mobile keyboard fix)
73+
const handleInputFocus = useCallback(() => {
74+
setTimeout(() => {
75+
inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
76+
}, 300)
77+
}, [])
78+
79+
return (
80+
<>
81+
<HeadSeo />
82+
83+
<div className="flex min-h-[100dvh] flex-col bg-gradient-to-b from-slate-50 to-slate-100">
84+
{/* Header */}
85+
<header className="flex items-center justify-between px-4 py-3 sm:px-6 sm:py-4">
86+
<div className="flex items-center gap-2">
87+
<DocsPlus size={32} />
88+
<span className="text-xl font-bold text-slate-800 sm:text-2xl">docs.plus</span>
89+
</div>
90+
91+
{isAuthServiceAvailable && (
92+
<div className="flex items-center gap-2">
93+
{user ? (
94+
<button
95+
onClick={() => setIsProfileOpen(true)}
96+
className="transition-transform hover:scale-105">
97+
<Avatar
98+
src={user.avatar_url}
99+
avatarUpdatedAt={user.avatar_updated_at}
100+
id={user.id}
101+
alt={user.display_name}
102+
clickable={false}
103+
className="size-10 rounded-full border-2 border-white shadow-md"
104+
/>
105+
</button>
106+
) : (
107+
<Button
108+
className="btn btn-primary btn-sm rounded-full px-4"
109+
onClick={() => setIsSignInOpen(true)}>
110+
Sign in
111+
</Button>
112+
)}
113+
</div>
114+
)}
115+
</header>
116+
117+
{/* Main Content */}
118+
<main className="flex flex-1 flex-col items-center justify-center px-4 py-8 sm:py-12">
119+
<div className="w-full max-w-2xl">
120+
{/* Hero Section */}
121+
<div className="mb-8 text-center sm:mb-12">
122+
<h1 className="mb-3 text-3xl font-bold text-slate-800 sm:text-4xl md:text-5xl">
123+
Get everyone on the same page
124+
</h1>
125+
<p className="text-base text-slate-500 sm:text-lg">
126+
Free, open-source collaborative documents for teams
127+
</p>
128+
</div>
129+
130+
{/* Action Card */}
131+
<div className="rounded-2xl bg-white p-6 shadow-xl shadow-slate-200/50 sm:p-8">
132+
{/* Quick Create */}
133+
<Button
134+
className="btn btn-primary btn-block mb-6 h-12 rounded-xl text-base font-semibold"
135+
onClick={() => navigateToDocument()}
136+
disabled={isLoading}>
137+
{isLoading ? <Loading size="sm" /> : 'Create New Document'}
138+
</Button>
139+
140+
{/* Divider */}
141+
<div className="mb-6 flex items-center gap-4">
142+
<div className="h-px flex-1 bg-slate-200" />
143+
<span className="text-sm text-slate-400">or open existing</span>
144+
<div className="h-px flex-1 bg-slate-200" />
145+
</div>
146+
147+
{/* Document Name Input */}
148+
<div className="flex flex-col gap-3 sm:flex-row">
149+
<div className="flex-1">
150+
<Input
151+
ref={inputRef}
152+
required
153+
pattern="[a-z0-9\-]*"
154+
minLength={3}
155+
maxLength={30}
156+
title="Only lowercase letters, numbers or dash"
157+
value={documentName}
158+
inputMode="text"
159+
enterKeyHint="go"
160+
placeholder="document-name"
161+
className="w-full"
162+
label={`${hostname}/`}
163+
labelPosition="before"
164+
onChange={handleInputChange}
165+
onKeyDown={handleKeyDown}
166+
onFocus={handleInputFocus}
167+
/>
168+
</div>
169+
<Button
170+
className="btn btn-neutral h-12 rounded-xl px-6 sm:h-auto"
171+
onClick={() => navigateToDocument()}
172+
disabled={isLoading || !documentName}>
173+
Open
174+
</Button>
175+
</div>
176+
</div>
177+
178+
{/* Info Links */}
179+
<div className="mt-8 space-y-2 text-center text-sm text-slate-500 sm:mt-10">
180+
<p>
181+
A{' '}
182+
<a
183+
href="https://github.com/docs-plus"
184+
className="font-medium text-blue-600 hover:underline">
185+
free & open source
186+
</a>{' '}
187+
project by{' '}
188+
<a
189+
href="https://newspeak.house"
190+
className="font-medium text-blue-600 hover:underline">
191+
Newspeak House
192+
</a>
193+
</p>
194+
<p>
195+
Seed funded by{' '}
196+
<a
197+
href="https://www.grantfortheweb.org"
198+
className="font-medium text-blue-600 hover:underline">
199+
Grant for Web
200+
</a>{' '}
201+
&{' '}
202+
<a
203+
href="https://www.nesta.org.uk"
204+
className="font-medium text-blue-600 hover:underline">
205+
Nesta
206+
</a>
207+
</p>
208+
</div>
209+
</div>
210+
</main>
211+
212+
{/* Footer */}
213+
<footer className="flex flex-wrap items-center justify-center gap-4 px-4 py-6 text-sm text-slate-500">
214+
<a
215+
href="https://github.com/docs-plus/docs.plus"
216+
className="flex items-center gap-1.5 rounded-full bg-slate-800 px-4 py-2 text-white transition-colors hover:bg-slate-700">
217+
<LuGithub size={18} />
218+
<span>Star on GitHub</span>
219+
</a>
220+
<a
221+
href="https://github.com/docs-plus/docs.plus/discussions"
222+
className="flex items-center gap-1.5 rounded-full border border-slate-300 px-4 py-2 transition-colors hover:bg-slate-100">
223+
<LuMessageCircle size={18} />
224+
<span>Discuss</span>
225+
</a>
226+
</footer>
227+
</div>
228+
229+
{/* Profile Modal */}
230+
{user && (
231+
<Modal
232+
asAChild={false}
233+
id="modal_profile"
234+
isOpen={isProfileOpen}
235+
setIsOpen={setIsProfileOpen}>
236+
<TabLayout name="profile" className="h-full max-h-[94%] max-w-[64rem]">
237+
<ProfilePanel />
238+
</TabLayout>
239+
</Modal>
240+
)}
241+
242+
{/* Sign In Modal */}
243+
{isAuthServiceAvailable && !user && (
244+
<Modal
245+
asAChild={false}
246+
id="modal_signin"
247+
isOpen={isSignInOpen}
248+
setIsOpen={setIsSignInOpen}>
249+
<TabLayout name="sign-in" footer={false} className="w-full p-6 sm:w-[28rem] sm:p-6">
250+
<SignInPanel />
251+
</TabLayout>
252+
</Modal>
253+
)}
254+
</>
255+
)
256+
}
257+
258+
export default HomePage

packages/webapp/src/components/pages/home/MobileView.tsx

Lines changed: 0 additions & 23 deletions
This file was deleted.

0 commit comments

Comments
 (0)