mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-12 17:51:53 +08:00
Compare commits
30 Commits
fix/activi
...
feat/preac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df44dad61f | ||
|
|
c5cda201ef | ||
|
|
f1de53c191 | ||
|
|
eaec8d70f7 | ||
|
|
5e690b854c | ||
|
|
cbb322b66f | ||
|
|
da9c897765 | ||
|
|
4c525f8dc0 | ||
|
|
00133369af | ||
|
|
5cdc443261 | ||
|
|
f354130f35 | ||
|
|
b2934993b0 | ||
|
|
b5d47349a1 | ||
|
|
8eadfb6640 | ||
|
|
407d70d462 | ||
|
|
d338480802 | ||
|
|
a4efe04a61 | ||
|
|
0688968c22 | ||
|
|
1d9cb45733 | ||
|
|
351e1e4509 | ||
|
|
410308bf7f | ||
|
|
e6054e247c | ||
|
|
174803dc2a | ||
|
|
77de8fdd3d | ||
|
|
678363f77d | ||
|
|
b424404bfd | ||
|
|
8102f60ebc | ||
|
|
130d460b94 | ||
|
|
52d110dffe | ||
|
|
5bd3d90d3c |
@@ -5,7 +5,9 @@ import compress from 'astro-compress';
|
||||
import { defineConfig } from 'astro/config';
|
||||
import rehypeExternalLinks from 'rehype-external-links';
|
||||
import { serializeSitemap, shouldIndexPage } from './sitemap.mjs';
|
||||
import preact from '@astrojs/preact';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://roadmap.sh/',
|
||||
markdown: {
|
||||
@@ -26,7 +28,7 @@ export default defineConfig({
|
||||
'https://github.com/kamranahmedse',
|
||||
'https://thenewstack.io',
|
||||
'https://cs.fyi',
|
||||
'https://roadmap.sh',
|
||||
'https://roadmap.sh'
|
||||
];
|
||||
|
||||
if (whiteListedStarts.some((start) => href.startsWith(start))) {
|
||||
@@ -56,5 +58,6 @@ export default defineConfig({
|
||||
css: false,
|
||||
js: false,
|
||||
}),
|
||||
preact(),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -111,7 +111,11 @@ async function run() {
|
||||
const topicTitle = group?.children?.controls?.control?.find(
|
||||
(control) => control?.typeID === 'Label'
|
||||
)?.properties?.text;
|
||||
const currTopicUrl = topicId.replace(/^\d+-/g, '/').replace(/:/g, '/');
|
||||
const currTopicUrl = topicId?.replace(/^\d+-/g, '/')?.replace(/:/g, '/');
|
||||
if (!currTopicUrl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const contentFilePath = topicUrlToPathMapping[currTopicUrl];
|
||||
|
||||
if (!contentFilePath) {
|
||||
|
||||
89
package.json
89
package.json
@@ -1,44 +1,49 @@
|
||||
{
|
||||
"name": "roadmap.sh",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"format": "prettier --write .",
|
||||
"astro": "astro",
|
||||
"deploy": "NODE_DEBUG=gh-pages gh-pages -d dist -t",
|
||||
"compress:jsons": "node bin/compress-jsons.cjs",
|
||||
"upgrade": "ncu -u",
|
||||
"roadmap-links": "node bin/roadmap-links.cjs",
|
||||
"roadmap-dirs": "node bin/roadmap-dirs.cjs",
|
||||
"roadmap-content": "node bin/roadmap-content.cjs",
|
||||
"best-practice-dirs": "node bin/best-practice-dirs.cjs",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "^1.2.1",
|
||||
"@astrojs/tailwind": "^3.1.1",
|
||||
"astro": "^2.1.7",
|
||||
"astro-compress": "^1.1.35",
|
||||
"node-html-parser": "^6.1.5",
|
||||
"npm-check-updates": "^16.8.0",
|
||||
"rehype-external-links": "^2.0.1",
|
||||
"roadmap-renderer": "^1.0.4",
|
||||
"tailwindcss": "^3.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.32.1",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"gh-pages": "^5.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"openai": "^3.2.1",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier-plugin-astro": "^0.8.0",
|
||||
"prettier-plugin-tailwindcss": "^0.2.6"
|
||||
}
|
||||
"name": "roadmap.sh",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"format": "prettier --write .",
|
||||
"astro": "astro",
|
||||
"deploy": "NODE_DEBUG=gh-pages gh-pages -d dist -t",
|
||||
"compress:jsons": "node bin/compress-jsons.cjs",
|
||||
"upgrade": "ncu -u",
|
||||
"roadmap-links": "node bin/roadmap-links.cjs",
|
||||
"roadmap-dirs": "node bin/roadmap-dirs.cjs",
|
||||
"roadmap-content": "node bin/roadmap-content.cjs",
|
||||
"best-practice-dirs": "node bin/best-practice-dirs.cjs",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/preact": "^2.1.0",
|
||||
"@astrojs/sitemap": "^1.2.1",
|
||||
"@astrojs/tailwind": "^3.1.1",
|
||||
"astro": "^2.1.7",
|
||||
"astro-compress": "^1.1.35",
|
||||
"jose": "^4.13.1",
|
||||
"js-cookie": "^3.0.1",
|
||||
"node-html-parser": "^6.1.5",
|
||||
"npm-check-updates": "^16.8.0",
|
||||
"preact": "^10.6.5",
|
||||
"rehype-external-links": "^2.0.1",
|
||||
"roadmap-renderer": "^1.0.4",
|
||||
"tailwindcss": "^3.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.32.1",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"gh-pages": "^5.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"openai": "^3.2.1",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier-plugin-astro": "^0.8.0",
|
||||
"prettier-plugin-tailwindcss": "^0.2.6"
|
||||
}
|
||||
}
|
||||
|
||||
4874
pnpm-lock.yaml
generated
4874
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
117
src/components/Login/LoginComponent.tsx
Normal file
117
src/components/Login/LoginComponent.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { FunctionComponent } from 'preact';
|
||||
import EmailLoginForm from './email-login-form';
|
||||
|
||||
export default function LoginComponent() {
|
||||
return (
|
||||
<div>
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-semibold leading-5 text-slate-900">
|
||||
Welcome back
|
||||
</h2>
|
||||
<p className="mt-2 text-sm leading-4 text-slate-600">
|
||||
Please enter your details.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 space-y-2">
|
||||
<GithubLoginButton />
|
||||
<GoogleLoginButton />
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<EmailLoginForm />
|
||||
|
||||
<div className="mt-6 text-center text-sm text-slate-600">
|
||||
Don't have an account?{' '}
|
||||
<a href="/signup" className="font-medium text-[#4285f4]">
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Divider: FunctionComponent<{ className?: string }> = ({
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex w-full items-center gap-2 py-6 text-sm text-slate-600">
|
||||
<div className="h-px w-full bg-slate-200" />
|
||||
OR
|
||||
<div className="h-px w-full bg-slate-200" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const GithubLoginButton: FunctionComponent<{ className?: string }> = ({
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<button className="inline-flex h-10 w-full items-center justify-center rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none transition duration-150 ease-in-out focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:opacity-60">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 96 96"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
|
||||
fill="#24292f"
|
||||
/>
|
||||
</svg>
|
||||
<span className="ml-2">Continue with Github</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const GoogleLoginButton: FunctionComponent<{ className?: string }> = ({
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<button className="inline-flex h-10 w-full items-center justify-center rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none transition duration-150 ease-in-out focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:opacity-60">
|
||||
<GoogleLogo />
|
||||
<span className="ml-2">Continue with Google</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
function GoogleLogo() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height={18}
|
||||
viewBox="0 0 186.69 190.5"
|
||||
>
|
||||
<g transform="translate(1184.583 765.171)">
|
||||
<path
|
||||
clipPath="none"
|
||||
mask="none"
|
||||
d="M-1089.333-687.239v36.888h51.262c-2.251 11.863-9.006 21.908-19.137 28.662l30.913 23.986c18.011-16.625 28.402-41.044 28.402-70.052 0-6.754-.606-13.249-1.732-19.483z"
|
||||
fill="#4285f4"
|
||||
/>
|
||||
<path
|
||||
clipPath="none"
|
||||
mask="none"
|
||||
d="M-1142.714-651.791l-6.972 5.337-24.679 19.223h0c15.673 31.086 47.796 52.561 85.03 52.561 25.717 0 47.278-8.486 63.038-23.033l-30.913-23.986c-8.486 5.715-19.31 9.179-32.125 9.179-24.765 0-45.806-16.712-53.34-39.226z"
|
||||
fill="#34a853"
|
||||
/>
|
||||
<path
|
||||
clipPath="none"
|
||||
mask="none"
|
||||
d="M-1174.365-712.61c-6.494 12.815-10.217 27.276-10.217 42.689s3.723 29.874 10.217 42.689c0 .086 31.693-24.592 31.693-24.592-1.905-5.715-3.031-11.776-3.031-18.098s1.126-12.383 3.031-18.098z"
|
||||
fill="#fbbc05"
|
||||
/>
|
||||
<path
|
||||
d="M-1089.333-727.244c14.028 0 26.497 4.849 36.455 14.201l27.276-27.276c-16.539-15.413-38.013-24.852-63.731-24.852-37.234 0-69.359 21.388-85.032 52.561l31.692 24.592c7.533-22.514 28.575-39.226 53.34-39.226z"
|
||||
fill="#ea4335"
|
||||
clipPath="none"
|
||||
mask="none"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
9
src/components/Login/LoginPopup.astro
Normal file
9
src/components/Login/LoginPopup.astro
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
import Popup from '../Popup/Popup.astro';
|
||||
import LoginComponent from './LoginComponent'
|
||||
---
|
||||
|
||||
<Popup id='login-popup' title='' subtitle=''>
|
||||
<LoginComponent client:only="preact" />
|
||||
</Popup>
|
||||
|
||||
56
src/components/Login/account-dropdown.tsx
Normal file
56
src/components/Login/account-dropdown.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/utils';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
export default function AccountDropdown() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// If user click outside the dropdown, and dropdown is open then close it.
|
||||
const handleOpen = () => {
|
||||
if (isOpen) setIsOpen(false);
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleOpen);
|
||||
return () => document.removeEventListener('click', handleOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
className="flex h-10 w-32 items-center justify-center rounded-full bg-gradient-to-r from-blue-500 to-blue-700 py-2 px-4 text-sm font-medium text-white hover:from-blue-500 hover:to-blue-600"
|
||||
onClick={() => setIsOpen((p) => !p)}
|
||||
>
|
||||
<span className="mr-2">Account</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={`absolute right-0 z-10 mt-2 w-48 rounded-md bg-slate-800 py-1 shadow-xl ${
|
||||
isOpen ? 'block' : 'hidden'
|
||||
}`}
|
||||
>
|
||||
<ul>
|
||||
<li className="px-1">
|
||||
<a
|
||||
href="/profile"
|
||||
className="block rounded px-4 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
|
||||
>
|
||||
Your Profile
|
||||
</a>
|
||||
</li>
|
||||
<li className="px-1">
|
||||
<button
|
||||
className="block w-full rounded px-4 py-2 text-left text-sm font-medium text-slate-100 hover:bg-slate-700"
|
||||
onClick={() => {
|
||||
Cookies.remove(TOKEN_COOKIE_NAME);
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
src/components/Login/account-nav.tsx
Normal file
57
src/components/Login/account-nav.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/utils';
|
||||
import AccountDropdown from './account-dropdown';
|
||||
|
||||
export default function AccountNavigation() {
|
||||
const { user, isLoading } = useAuth();
|
||||
|
||||
console.log('user', user, isLoading);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isLoading ? (
|
||||
<div className="flex h-10 w-32 items-center justify-center rounded-full bg-gradient-to-r from-blue-500 to-blue-700 py-2 px-4 text-sm text-white hover:from-blue-500 hover:to-blue-600">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{user ? (
|
||||
<AccountDropdown />
|
||||
) : (
|
||||
<a
|
||||
className="flex h-10 w-32 cursor-pointer items-center justify-center rounded-full bg-gradient-to-r from-blue-500 to-blue-700 py-2 px-4 text-sm font-medium text-white hover:from-blue-500 hover:to-blue-600"
|
||||
href="/signup"
|
||||
>
|
||||
<span className="mr-2">Register</span>
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Spinner() {
|
||||
return (
|
||||
<svg
|
||||
className="h-5 w-5 animate-spin text-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="stroke-[4px] opacity-25"
|
||||
cx={12}
|
||||
cy={12}
|
||||
r={10}
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
84
src/components/Login/email-login-form.tsx
Normal file
84
src/components/Login/email-login-form.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import type { FunctionComponent } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/utils';
|
||||
|
||||
const EmailLoginForm: FunctionComponent<{}> = () => {
|
||||
const [email, setEmail] = useState<string>('');
|
||||
const [password, setPassword] = useState<string>('');
|
||||
const [error, setError] = useState<{
|
||||
message: string;
|
||||
status: number;
|
||||
} | null>(null);
|
||||
|
||||
return (
|
||||
<form
|
||||
className="w-full"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
fetch('http://localhost:8080/v1-login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
// name: 'Arikko',
|
||||
}),
|
||||
}).then(async (res) => {
|
||||
const json = await res.json();
|
||||
if (res.status === 200) {
|
||||
Cookies.set(TOKEN_COOKIE_NAME, json.token);
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
console.log('error', json);
|
||||
setError(json);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<label htmlFor="email" className="sr-only">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
className="block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
|
||||
placeholder="Enter you email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(String((e.target as any).value))}
|
||||
/>
|
||||
<label htmlFor="password" className="sr-only">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
|
||||
placeholder="Enter you password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(String((e.target as any).value))}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div className="mt-2 text-sm text-red-500">{error.message}</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-3 inline-flex h-10 w-full items-center justify-center rounded-lg border border-slate-300 bg-black p-2 text-sm font-medium text-white outline-none transition duration-150 ease-in-out focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:opacity-60"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailLoginForm;
|
||||
@@ -1,21 +1,24 @@
|
||||
---
|
||||
import Icon from './Icon.astro';
|
||||
import AccountNavigation from './Login/account-nav';
|
||||
---
|
||||
|
||||
<div class='bg-slate-900 text-white py-5 sm:py-8'>
|
||||
<div class='bg-slate-900 py-5 text-white sm:py-8'>
|
||||
<nav class='container flex items-center justify-between'>
|
||||
<a class='font-medium text-lg flex items-center text-white' href='/'>
|
||||
<a class='flex items-center text-lg font-medium text-white' href='/'>
|
||||
<Icon icon='logo' />
|
||||
<span class='ml-3'>roadmap.sh</span>
|
||||
</a>
|
||||
|
||||
<!-- Desktop navigation items -->
|
||||
<ul class='hidden sm:flex space-x-5'>
|
||||
<ul class='hidden space-x-5 sm:flex sm:items-center'>
|
||||
<li>
|
||||
<a href='/roadmaps' class='text-gray-400 hover:text-white'>Roadmaps</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='/best-practices' class='text-gray-400 hover:text-white'>Best Practices</a>
|
||||
<a href='/best-practices' class='text-gray-400 hover:text-white'
|
||||
>Best Practices</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href='/guides' class='text-gray-400 hover:text-white'>Guides</a>
|
||||
@@ -24,44 +27,59 @@ import Icon from './Icon.astro';
|
||||
<a href='/videos' class='text-gray-400 hover:text-white'>Videos</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class='py-2 px-4 text-sm font-regular rounded-full bg-gradient-to-r from-blue-500 to-blue-700 hover:from-blue-500 hover:to-blue-600 text-white'
|
||||
href='/signup'
|
||||
>
|
||||
Subscribe
|
||||
</a>
|
||||
<AccountNavigation client:load />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Mobile Navigation Button -->
|
||||
<button class='text-gray-400 hover:text-gray-50 block sm:hidden cursor-pointer' aria-label='Menu' show-mobile-nav>
|
||||
<button
|
||||
class='block cursor-pointer text-gray-400 hover:text-gray-50 sm:hidden'
|
||||
aria-label='Menu'
|
||||
show-mobile-nav
|
||||
>
|
||||
<Icon icon='hamburger' />
|
||||
</button>
|
||||
|
||||
<!-- Mobile Navigation Items -->
|
||||
<div class='fixed top-0 bottom-0 left-0 right-0 z-40 bg-slate-900 items-center flex hidden' mobile-nav>
|
||||
<div
|
||||
class='fixed top-0 bottom-0 left-0 right-0 z-40 flex hidden items-center bg-slate-900'
|
||||
mobile-nav
|
||||
>
|
||||
<button
|
||||
close-mobile-nav
|
||||
class='text-gray-400 hover:text-gray-50 block cursor-pointer absolute top-6 right-6'
|
||||
class='absolute top-6 right-6 block cursor-pointer text-gray-400 hover:text-gray-50'
|
||||
aria-label='Close Menu'
|
||||
>
|
||||
<Icon icon='close' />
|
||||
</button>
|
||||
<ul class='flex flex-col gap-2 md:gap-3 items-center w-full'>
|
||||
<ul class='flex w-full flex-col items-center gap-2 md:gap-3'>
|
||||
<li>
|
||||
<a href='/roadmaps' class='text-xl md:text-lg hover:text-blue-300'>Roadmaps</a>
|
||||
<a href='/roadmaps' class='text-xl hover:text-blue-300 md:text-lg'
|
||||
>Roadmaps</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href='/best-practices' class='text-xl md:text-lg hover:text-blue-300'>Best Practices</a>
|
||||
<a
|
||||
href='/best-practices'
|
||||
class='text-xl hover:text-blue-300 md:text-lg'>Best Practices</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href='/guides' class='text-xl md:text-lg hover:text-blue-300'>Guides</a>
|
||||
<a href='/guides' class='text-xl hover:text-blue-300 md:text-lg'
|
||||
>Guides</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href='/videos' class='text-xl md:text-lg hover:text-blue-300'>Videos</a>
|
||||
<a href='/videos' class='text-xl hover:text-blue-300 md:text-lg'
|
||||
>Videos</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href='/signup' class='text-xl md:text-lg text-red-300 hover:text-red-400'>Subscribe</a>
|
||||
<a
|
||||
href='/signup'
|
||||
class='text-xl text-red-300 hover:text-red-400 md:text-lg'
|
||||
>Subscribe</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -73,7 +91,9 @@ import Icon from './Icon.astro';
|
||||
document.querySelector('[mobile-nav]')?.classList.remove('hidden');
|
||||
});
|
||||
|
||||
document.querySelector('[close-mobile-nav]')?.addEventListener('click', () => {
|
||||
document.querySelector('[mobile-nav]')?.classList.add('hidden');
|
||||
});
|
||||
document
|
||||
.querySelector('[close-mobile-nav]')
|
||||
?.addEventListener('click', () => {
|
||||
document.querySelector('[mobile-nav]')?.classList.add('hidden');
|
||||
});
|
||||
</script>
|
||||
|
||||
45
src/components/Profile/profile-details.tsx
Normal file
45
src/components/Profile/profile-details.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
|
||||
export default function ProfileDetails() {
|
||||
const { user, isLoading } = useAuth();
|
||||
return (
|
||||
<div className="py-10 pb-20">
|
||||
<h1 className="text-3xl font-bold sm:text-4xl">Profile</h1>
|
||||
<p className="mt-2">Here you can view your profile details.</p>
|
||||
<div className="mt-5 space-y-4">
|
||||
<div>
|
||||
<label className="text-slate-500">Name</label>
|
||||
<div className="mt-1">
|
||||
{isLoading || !user ? (
|
||||
<Skeleton />
|
||||
) : (
|
||||
<h2 className="text-xl font-medium text-slate-800">
|
||||
{user?.name}
|
||||
</h2>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-slate-500">Email</label>
|
||||
<div className="mt-1">
|
||||
{isLoading || !user ? (
|
||||
<Skeleton className="w-64" />
|
||||
) : (
|
||||
<h2 className="text-xl font-medium text-slate-800">
|
||||
{user?.email}
|
||||
</h2>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Skeleton({ className }: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={`h-7 w-36 animate-pulse rounded-md bg-slate-100 ${className}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
---
|
||||
import DownloadPopup from './DownloadPopup.astro';
|
||||
import Icon from './Icon.astro';
|
||||
import LoginComponent from './Login/LoginComponent';
|
||||
import LoginPopup from './Login/LoginPopup.astro';
|
||||
import RoadmapHint from './RoadmapHint.astro';
|
||||
import RoadmapNote from './RoadmapNote.astro';
|
||||
import SubscribePopup from './SubscribePopup.astro';
|
||||
@@ -18,23 +20,33 @@ export interface Props {
|
||||
hasTopics?: boolean;
|
||||
}
|
||||
|
||||
const { title, description, roadmapId, tnsBannerLink, isUpcoming = false, hasSearch = false, note, hasTopics = false } = Astro.props;
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
roadmapId,
|
||||
tnsBannerLink,
|
||||
isUpcoming = false,
|
||||
hasSearch = false,
|
||||
note,
|
||||
hasTopics = false,
|
||||
} = Astro.props;
|
||||
|
||||
const isRoadmapReady = !isUpcoming;
|
||||
---
|
||||
|
||||
<LoginPopup />
|
||||
<DownloadPopup />
|
||||
<SubscribePopup />
|
||||
|
||||
<div class='border-b'>
|
||||
<div class='py-5 sm:py-12 container relative'>
|
||||
<div class='container relative py-5 sm:py-12'>
|
||||
<YouTubeAlert />
|
||||
|
||||
<div class='mt-0 mb-3 sm:mb-4 sm:mt-4'>
|
||||
<h1 class='text-2xl sm:text-4xl mb-0.5 sm:mb-2 font-bold'>
|
||||
<h1 class='mb-0.5 text-2xl font-bold sm:mb-2 sm:text-4xl'>
|
||||
{title}
|
||||
</h1>
|
||||
<p class='text-gray-500 text-sm sm:text-lg'>{description}</p>
|
||||
<p class='text-sm text-gray-500 sm:text-lg'>{description}</p>
|
||||
</div>
|
||||
|
||||
<div class='flex justify-between'>
|
||||
@@ -44,7 +56,7 @@ const isRoadmapReady = !isUpcoming;
|
||||
<>
|
||||
<a
|
||||
href='/roadmaps'
|
||||
class='bg-gray-500 py-1.5 px-3 rounded-md text-white text-xs sm:text-sm font-medium hover:bg-gray-600'
|
||||
class='rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm'
|
||||
aria-label='Back to All Roadmaps'
|
||||
>
|
||||
←<span class='hidden sm:inline'> All Roadmaps</span>
|
||||
@@ -52,21 +64,38 @@ const isRoadmapReady = !isUpcoming;
|
||||
|
||||
{isRoadmapReady && (
|
||||
<button
|
||||
data-popup='download-popup'
|
||||
class='inline-flex items-center justify-center bg-yellow-400 py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-yellow-500'
|
||||
id='download-button'
|
||||
data-popup='login-popup'
|
||||
class='inline-flex items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm'
|
||||
aria-label='Download Roadmap'
|
||||
ga-category='Subscription'
|
||||
ga-action='Clicked Popup Opener'
|
||||
ga-label='Download Roadmap Popup'
|
||||
>
|
||||
<Icon icon='download' />
|
||||
<span class='hidden sm:inline ml-2'>Download</span>
|
||||
<span class='ml-2 hidden sm:inline'>Download</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isRoadmapReady && (
|
||||
<a
|
||||
id='download-link'
|
||||
class='inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm'
|
||||
aria-label='Download Roadmap'
|
||||
ga-category='Subscription'
|
||||
ga-action='Clicked Popup Opener'
|
||||
ga-label='Download Roadmap Popup'
|
||||
href='/pdfs'
|
||||
>
|
||||
<Icon icon='download' />
|
||||
<span class='ml-2 hidden sm:inline'>Download</span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
<button
|
||||
data-popup='subscribe-popup'
|
||||
class='inline-flex items-center justify-center bg-yellow-400 py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-yellow-500'
|
||||
id='subscribe-button'
|
||||
data-popup='login-popup'
|
||||
class='inline-flex items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm'
|
||||
aria-label='Subscribe for Updates'
|
||||
ga-category='Subscription'
|
||||
ga-action='Clicked Popup Opener'
|
||||
@@ -83,7 +112,7 @@ const isRoadmapReady = !isUpcoming;
|
||||
hasSearch && (
|
||||
<a
|
||||
href={`/${roadmapId}`}
|
||||
class='bg-gray-500 py-1.5 px-3 rounded-md text-white text-xs sm:text-sm font-medium hover:bg-gray-600'
|
||||
class='rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm'
|
||||
aria-label='Back to Visual Roadmap'
|
||||
>
|
||||
←
|
||||
@@ -98,7 +127,7 @@ const isRoadmapReady = !isUpcoming;
|
||||
<a
|
||||
href={`https://github.com/kamranahmedse/developer-roadmap/issues/new?title=[Suggestion] ${title}`}
|
||||
target='_blank'
|
||||
class='inline-flex items-center justify-center bg-gray-500 text-white py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-gray-600'
|
||||
class='inline-flex items-center justify-center rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm'
|
||||
aria-label='Suggest Changes'
|
||||
>
|
||||
<Icon icon='comment' class='h-3 w-3' />
|
||||
@@ -110,10 +139,30 @@ const isRoadmapReady = !isUpcoming;
|
||||
</div>
|
||||
|
||||
<!-- Desktop: Roadmap Resources - Alert -->
|
||||
{hasTopics && <RoadmapHint roadmapId={roadmapId} tnsBannerLink={tnsBannerLink} />}
|
||||
{
|
||||
hasTopics && (
|
||||
<RoadmapHint roadmapId={roadmapId} tnsBannerLink={tnsBannerLink} />
|
||||
)
|
||||
}
|
||||
|
||||
{hasSearch && <TopicSearch />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{note && <RoadmapNote text={note} />}
|
||||
|
||||
<script>
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE_NAME } from '../lib/utils';
|
||||
|
||||
const subscribeButton = document.getElementById('subscribe-button');
|
||||
const downloadButton = document.getElementById('download-button');
|
||||
const downloadLink = document.getElementById('download-link');
|
||||
const token = Cookies.get(TOKEN_COOKIE_NAME);
|
||||
|
||||
if (token) {
|
||||
subscribeButton?.classList.add('hidden');
|
||||
downloadButton?.classList.add('hidden');
|
||||
downloadLink?.classList.remove('hidden');
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -21,7 +21,7 @@ const { contentContributionLink } = Astro.props;
|
||||
|
||||
<div id='topic-actions' class='hidden mb-2'>
|
||||
<button
|
||||
id='mark-topic-done'
|
||||
data-popup='login-popup'
|
||||
ga-category='TopicClick'
|
||||
ga-action='topic/mark-completion'
|
||||
ga-label='done'
|
||||
@@ -36,7 +36,7 @@ const { contentContributionLink } = Astro.props;
|
||||
ga-category='TopicClick'
|
||||
ga-action='topic/mark-completion'
|
||||
ga-label='pending'
|
||||
class='hidden bg-red-600 text-white p-1 px-2 text-sm rounded-md hover:bg-red-700 inline-flex items-center'
|
||||
class='hidden bg-red-600 text-white p-1 px-2 text-sm rounded-md hover:bg-red-700 items-center'
|
||||
>
|
||||
<Icon icon='reset' />
|
||||
<span class='ml-2'>Mark as Pending</span>
|
||||
|
||||
@@ -1 +1,33 @@
|
||||
# Saml
|
||||
## Security Assertion Markup Language (SAML)
|
||||
|
||||
**SAML** stands for Security Assertion Markup Language. It is an XML-based standard for exchanging authentication and authorization data between parties, particularly between an identity provider (IdP) and a service provider (SP). In a SAML-based system, a user requests access to a protected resource. The service provider asks the identity provider to authenticate the user and assert whether they are granted access to the resource.
|
||||
|
||||
### Benefits of SAML
|
||||
|
||||
Some advantages of using SAML include:
|
||||
|
||||
- Single Sign-On (SSO): Users can log in once at the IdP and access multiple service providers without needing to authenticate again.
|
||||
- Improved security: Passwords and user credentials are not required to be stored and managed by the service provider, reducing the potential vectors for attack.
|
||||
- Increased efficiency: As users no longer need to maintain multiple sets of credentials, managing access becomes easier for both the user and the system administrators.
|
||||
- Interoperability: SAML enables a wide range of applications to work together, regardless of the underlying technology or platform.
|
||||
|
||||
### SAML Components
|
||||
|
||||
Three main components are involved in the SAML architecture:
|
||||
|
||||
1. **Identity Provider (IdP)**: The entity that manages users' identities and authenticates them by providing security tokens, also called assertions.
|
||||
2. **Service Provider (SP)**: The entity that provides a service (such as a web application or API) and relies on the identity provider to authenticate users and grant/deny access to the resources.
|
||||
3. **User/Principal**: The end user seeking access to the service provided by the service provider.
|
||||
|
||||
### SAML Workflow
|
||||
|
||||
The SAML authentication process consists of the following steps:
|
||||
|
||||
1. The user requests access to a protected resource from the service provider.
|
||||
2. If the user is not already authenticated, the service provider generates and sends a SAML authentication request to the identity provider.
|
||||
3. The identity provider authenticates the user (using, e.g., a username and password, multi-factor authentication, or another method).
|
||||
4. The identity provider constructs a SAML response, which includes details about the user and asserts whether the user is authorized to access the requested resource.
|
||||
5. The SAML response is sent back to the service provider, typically via the user's web browser or API client.
|
||||
6. The service provider processes the SAML response, extracts the necessary information, and grants or denies access to the user based on the identity provider's assertion.
|
||||
|
||||
With SAML, you can streamline user authentication and authorization across various applications and systems, providing a better user experience and improving your overall backend security.
|
||||
|
||||
@@ -1 +1,37 @@
|
||||
# Design and development principles
|
||||
# Design and Development Principles
|
||||
|
||||
In this section, we'll discuss some essential design and development principles to follow while building the backend of any application. These principles will ensure that the backend is efficient, scalable, and maintainable.
|
||||
|
||||
## 1. Separation of Concerns (SoC)
|
||||
|
||||
Separation of Concerns is a fundamental principle that states that different functionalities of a system should be as independent as possible. This approach improves maintainability and scalability by allowing developers to work on separate components without affecting each other. Divide your backend into clear modules and layers, such as data storage, business logic, and network communication.
|
||||
|
||||
## 2. Reusability
|
||||
|
||||
Reusability is the ability to use components, functions, or modules in multiple places without duplicating code. While designing the backend, look for opportunities where you can reuse existing code. Use techniques like creating utility functions, abstract classes, and interfaces to promote reusability and reduce redundancy.
|
||||
|
||||
## 3. Keep It Simple and Stupid (KISS)
|
||||
|
||||
KISS principle states that the simpler the system, the easier it is to understand, maintain, and extend. When designing the backend, try to keep the architecture and code as simple as possible. Use clear naming conventions and modular structures, and avoid over-engineering and unnecessary complexity.
|
||||
|
||||
## 4. Don't Repeat Yourself (DRY)
|
||||
|
||||
Do not duplicate code or functionality across your backend. Duplication can lead to inconsistency and maintainability issues. Instead, focus on creating reusable components, functions or modules, which can be shared across different parts of the backend.
|
||||
|
||||
## 5. Scalability
|
||||
|
||||
A scalable system is one that can efficiently handle an increasing number of users, requests, or data. Design the backend with scalability in mind, considering factors such as data storage, caching, load balancing, and horizontal scaling (adding more instances of the backend server).
|
||||
|
||||
## 6. Security
|
||||
|
||||
Security is a major concern when developing any application. Always follow best practices to prevent security flaws, such as protecting sensitive data, using secure communication protocols (e.g., HTTPS), implementing authentication and authorization mechanisms, and sanitizing user inputs.
|
||||
|
||||
## 7. Testing
|
||||
|
||||
Testing is crucial for ensuring the reliability and stability of the backend. Implement a comprehensive testing strategy, including unit, integration, and performance tests. Use automated testing tools and set up continuous integration (CI) and continuous deployment (CD) pipelines to streamline the testing and deployment process.
|
||||
|
||||
## 8. Documentation
|
||||
|
||||
Proper documentation helps developers understand and maintain the backend codebase. Write clear and concise documentation for your code, explaining the purpose, functionality, and how to use it. Additionally, use comments and appropriate naming conventions to make the code itself more readable and self-explanatory.
|
||||
|
||||
By following these design and development principles, you'll be well on your way to creating an efficient, secure, and maintainable backend for your applications.
|
||||
|
||||
@@ -1 +1,29 @@
|
||||
# Search engines
|
||||
# Search Engines
|
||||
|
||||
Search engines are an essential part of any web application, responsible for providing efficient and relevant search results for users. They store and retrieve data based on unique indexes, which allow for fast and accurate searches. As a backend developer, understanding search engines functionalities, and how to integrate them into your web application, is crucial.
|
||||
|
||||
## Types of Search Engines
|
||||
|
||||
There are two primary types of search engines:
|
||||
|
||||
1. **Full-text search engines**: These are specifically designed for searching and analyzing text documents. They can efficiently index large volumes of text and provide relevant results based on keywords or phrases. Popular full-text search engines examples include **Elasticsearch**, **Solr**, and **Amazon CloudSearch**.
|
||||
|
||||
2. **Database search engines**: Database engines are built-in features of most databases. They provide search capabilities within the data stored in the database. Examples include **MySQL FULLTEXT search** and **PostgreSQL Full-Text Search**.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
When dealing with search engines, it's important to understand these key concepts:
|
||||
|
||||
- **Indexing**: The process of analyzing and storing data in an optimized format for fast search and retrieval.
|
||||
- **Tokenization**: Breaking text into individual words or terms (also known as tokens), for efficient indexing and searching.
|
||||
- **Querying**: The act of searching the indexed data by asking a specific question or requesting information based on keywords or phrases.
|
||||
- **Relevance scoring**: A score assigned to each search result that indicates how closely it matches the query, based on algorithms and relevance models.
|
||||
|
||||
## Integration
|
||||
|
||||
To integrate a search engine into your web application, you would typically follow these steps:
|
||||
|
||||
1. **Choose the search engine**: Identify the search engine that best suits your application's needs, considering factors such as scalability, performance, and ease of integration.
|
||||
2. **Index your data**: Analyze and store your data using the chosen search engine. This process may involve creating an index, specifying fields, and defining how the data should be tokenized and analyzed.
|
||||
3. **Implement search functionality**: Develop the backend code for handling search requests, such as sending queries to the search engine and parsing the responses. Additionally, make sure to handle user inputs, like keywords, phrases, and filters.
|
||||
4. **Display search results**: Design the frontend of your application to show search results in a user-friendly manner, including pagination, sorting, and filters.
|
||||
|
||||
17
src/hooks/use-auth.ts
Normal file
17
src/hooks/use-auth.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { TOKEN_COOKIE_NAME, TokenPayload, decodeToken } from '../lib/utils';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
export const useAuth = () => {
|
||||
const [user, setUser] = useState<TokenPayload | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const token = Cookies.get(TOKEN_COOKIE_NAME);
|
||||
const payload = token ? decodeToken(token) : null;
|
||||
setUser(payload);
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
return { user, isLoading };
|
||||
};
|
||||
14
src/lib/utils.ts
Normal file
14
src/lib/utils.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import * as jose from 'jose';
|
||||
|
||||
export const TOKEN_COOKIE_NAME = '__roadmapsh_jt__';
|
||||
export type TokenPayload = {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export function decodeToken(token: string): TokenPayload {
|
||||
const claims = jose.decodeJwt(token);
|
||||
|
||||
return claims as TokenPayload;
|
||||
}
|
||||
10
src/pages/profile/index.astro
Normal file
10
src/pages/profile/index.astro
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
import ProfileDetails from '../../components/Profile/profile-details';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Profile">
|
||||
<div class="container">
|
||||
<ProfileDetails client:idle />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -1,6 +1,12 @@
|
||||
---
|
||||
import CaptchaFields from '../components/Captcha/CaptchaFields.astro';
|
||||
import CaptchaScripts from '../components/Captcha/CaptchaScripts.astro';
|
||||
import {
|
||||
Divider,
|
||||
GithubLoginButton,
|
||||
GoogleLoginButton,
|
||||
} from '../components/Login/LoginComponent';
|
||||
import EmailLoginForm from '../components/Login/email-login-form';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
@@ -12,52 +18,71 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
>
|
||||
<div class='container'>
|
||||
<div
|
||||
class='py-12 sm:py-0 sm:min-h-[550px] sm:max-w-[400px] mx-auto flex items-start sm:items-center flex-col justify-start sm:justify-center'
|
||||
class='mx-auto flex flex-col items-start justify-start py-12 sm:min-h-[550px] sm:max-w-[400px] sm:items-center sm:justify-center sm:py-0'
|
||||
>
|
||||
<div class='mb-2 sm:mb-5 text-left sm:text-center'>
|
||||
<h1 class='text-3xl sm:text-5xl font-semibold mb-2 sm:mb-4'>Signup</h1>
|
||||
<p class='hidden sm:block text-md text-gray-600'>
|
||||
Register yourself to receive occasional emails about new roadmaps, updates, guides and videos
|
||||
<div class='mb-2 text-left sm:mb-5 sm:text-center'>
|
||||
<h1 class='mb-2 text-3xl font-semibold sm:mb-4 sm:text-5xl'>Signup</h1>
|
||||
<p class='text-md hidden text-gray-600 sm:block'>
|
||||
Register yourself to receive occasional emails about new roadmaps,
|
||||
updates, guides and videos
|
||||
</p>
|
||||
<p class='text-sm block sm:hidden text-gray-600'>
|
||||
Register yourself for occasional updates about roadmaps, guides and videos.
|
||||
<p class='block text-sm text-gray-600 sm:hidden'>
|
||||
Register yourself for occasional updates about roadmaps, guides and
|
||||
videos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
action='https://news.roadmap.sh/subscribe'
|
||||
method='POST'
|
||||
accept-charset='utf-8'
|
||||
class='w-full'
|
||||
captcha-form
|
||||
>
|
||||
<input type='hidden' name='gdpr' value='true' />
|
||||
<div class='w-full space-y-2'>
|
||||
<button login-google>login with google</button>
|
||||
|
||||
<input
|
||||
type='email'
|
||||
required
|
||||
name='email'
|
||||
id='email'
|
||||
autofocus
|
||||
class='mt-1 block w-full mb-2 border-2 rounded-md py-2 sm:py-3 px-3 sm:px-3.5 text-md'
|
||||
placeholder='Enter your email'
|
||||
/>
|
||||
<GithubLoginButton />
|
||||
<GoogleLoginButton />
|
||||
</div>
|
||||
|
||||
<CaptchaFields />
|
||||
<Divider />
|
||||
|
||||
<input type='hidden' name='list' value='tTqz1w7nexY3cWDpLnI88Q' />
|
||||
<input type='hidden' name='subform' value='yes' />
|
||||
|
||||
<button
|
||||
type='submit'
|
||||
name='submit'
|
||||
class='bg-gradient-to-r from-blue-500 to-blue-700 hover:from-blue-600 hover:to-blue-800 text-white py-2 sm:py-2.5 sm:px-5 rounded-md w-full text-md'
|
||||
>
|
||||
Subscribe
|
||||
</button>
|
||||
</form>
|
||||
<EmailLoginForm client:load />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CaptchaScripts slot='after-footer' />
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
document
|
||||
.querySelector('button[login-google]')
|
||||
?.addEventListener('click', () => {
|
||||
fetch('http://localhost:8080/v1-google-login', {
|
||||
credentials: 'include',
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
window.location.href = data.loginUrl;
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
// Get all query params and send them to v1-google-callback
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code');
|
||||
const state = urlParams.get('state');
|
||||
const prompt = urlParams.get('prompt');
|
||||
|
||||
if (code && state && prompt) {
|
||||
fetch(
|
||||
`http://localhost:8080/v1-google-callback${window.location.search}`,
|
||||
{
|
||||
method: 'get',
|
||||
credentials: 'include',
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
console.log(data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
}
|
||||
|
||||
.prose ul li > code,
|
||||
p > code {
|
||||
p > code,
|
||||
a > code {
|
||||
background: #ebebeb !important;
|
||||
color: currentColor !important;
|
||||
font-size: 14px;
|
||||
@@ -19,24 +20,12 @@ p > code {
|
||||
.prose ul li > code:before,
|
||||
p > code:before,
|
||||
.prose ul li > code:after,
|
||||
p > code:after {
|
||||
p > code:after,
|
||||
a > code:after,
|
||||
a > code:before {
|
||||
content: '' !important;
|
||||
}
|
||||
|
||||
.bg-stripes {
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
var(--stripes-color) 12.5%,
|
||||
transparent 12.5%,
|
||||
transparent 50%,
|
||||
var(--stripes-color) 50%,
|
||||
var(--stripes-color) 62.5%,
|
||||
transparent 62.5%,
|
||||
transparent 100%
|
||||
);
|
||||
background-size: 5.66px 5.66px;
|
||||
}
|
||||
|
||||
.sponsor-footer {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user