Compare commits
3 Commits
283417fe24
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8449377b6d | |||
| 3b5f140d21 | |||
| 740753cb33 |
+38
-23
@@ -19,11 +19,12 @@ jobs:
|
|||||||
mkcert -install
|
mkcert -install
|
||||||
mkcert -cert-file certs/localhost.pem -key-file certs/localhost-key.pem localhost 127.0.0.1 ::1
|
mkcert -cert-file certs/localhost.pem -key-file certs/localhost-key.pem localhost 127.0.0.1 ::1
|
||||||
|
|
||||||
- name: Cache certificates
|
- name: Upload certificates
|
||||||
uses: actions/cache/save@v4
|
uses: christopherHX/gitea-upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: certs
|
name: ssl-certs
|
||||||
key: certs-${{ runner.os }}-${{ github.sha }}
|
path: certs/
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
name: Frontend CI
|
name: Frontend CI
|
||||||
@@ -37,10 +38,10 @@ jobs:
|
|||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
- name: Restore certificates
|
- name: Restore certificates
|
||||||
uses: actions/cache/restore@v4
|
uses: christopherHX/gitea-download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: certs
|
name: ssl-certs
|
||||||
key: certs-${{ runner.os }}-${{ github.sha }}
|
path: certs/
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
@@ -61,15 +62,15 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: setup-environment
|
needs: setup-environment
|
||||||
services:
|
services:
|
||||||
postgres:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
env:
|
env:
|
||||||
POSTGRES_DB: piku
|
POSTGRES_DB: piku__test
|
||||||
POSTGRES_USER: user
|
POSTGRES_USER: test
|
||||||
POSTGRES_PASSWORD: password123
|
POSTGRES_PASSWORD: password123
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5442:5432
|
||||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
options: --tmpfs /var/lib/postgresql/data --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./backend
|
working-directory: ./backend
|
||||||
@@ -82,18 +83,28 @@ jobs:
|
|||||||
cache-dependency-glob: "backend/uv.lock"
|
cache-dependency-glob: "backend/uv.lock"
|
||||||
|
|
||||||
- name: Restore certificates
|
- name: Restore certificates
|
||||||
uses: actions/cache/restore@v4
|
uses: christopherHX/gitea-download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: certs
|
name: ssl-certs
|
||||||
key: certs-${{ runner.os }}-${{ github.sha }}
|
path: certs/
|
||||||
|
|
||||||
- name: Setup Environment
|
- name: Setup & Test
|
||||||
run: |
|
run: |
|
||||||
cp ../.env.example ../.env
|
cp ../.env.example ../.env
|
||||||
uv sync
|
uv sync
|
||||||
|
|
||||||
- name: Lint & Test
|
export DB_NAME="piku__test"
|
||||||
run: |
|
export DB_USER="test"
|
||||||
|
export DB_PASSWORD="password123"
|
||||||
|
|
||||||
|
if [ "$GITEA_ACTIONS" = "true" ]; then
|
||||||
|
export DB_HOST="db"
|
||||||
|
export DB_PORT="5432"
|
||||||
|
else
|
||||||
|
export DB_HOST="127.0.0.1"
|
||||||
|
export DB_PORT="5442"
|
||||||
|
fi
|
||||||
|
|
||||||
uv run ruff check
|
uv run ruff check
|
||||||
uv run python manage.py test
|
uv run python manage.py test
|
||||||
|
|
||||||
@@ -101,23 +112,27 @@ jobs:
|
|||||||
name: E2E Tests
|
name: E2E Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: setup-environment
|
needs: setup-environment
|
||||||
|
# Skipping on Gitea pushes until cache server is configured
|
||||||
|
if: github.server_url == 'https://github.com' || github.event_name == 'pull_request'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Restore Certificates
|
- name: Restore Certificates
|
||||||
uses: actions/cache/restore@v4
|
uses: christopherHX/gitea-download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: certs
|
name: ssl-certs
|
||||||
key: certs-${{ runner.os }}-${{ github.sha }}
|
path: certs/
|
||||||
|
|
||||||
- name: Setup Tools
|
- name: Setup Tools
|
||||||
uses: astral-sh/setup-uv@v5
|
uses: astral-sh/setup-uv@v5
|
||||||
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
|
|
||||||
- name: Cache Playwright
|
- name: Cache Playwright
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
|
# Disable cache when not using GitHub Actions because the runner spends ~3mins trying to upload the cache and failing
|
||||||
|
# TODO: setup cache server in Gitea
|
||||||
|
if: github.server_url == 'https://github.com'
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/ms-playwright
|
path: ~/.cache/ms-playwright
|
||||||
@@ -140,7 +155,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Playwright Report
|
- name: Upload Playwright Report
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: christopherHX/gitea-upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: playwright-report
|
name: playwright-report
|
||||||
path: frontend/playwright-report/
|
path: frontend/playwright-report/
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import { lazy, Suspense, useEffect, useRef } from "react";
|
import { lazy, Suspense, useEffect, useRef } from "react";
|
||||||
import {
|
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
||||||
BrowserRouter,
|
|
||||||
Navigate,
|
|
||||||
Route,
|
|
||||||
Routes,
|
|
||||||
ScrollRestoration,
|
|
||||||
} from "react-router-dom";
|
|
||||||
import { ProtectedRoute, PublicRoute } from "./components/RouteGuards";
|
import { ProtectedRoute, PublicRoute } from "./components/RouteGuards";
|
||||||
import SplashScreen from "./components/SplashScreen";
|
import SplashScreen from "./components/SplashScreen";
|
||||||
import { ROUTES } from "./config/routes";
|
import { ROUTES } from "./config/routes";
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import { LockKeyIcon } from "@phosphor-icons/react";
|
import { HourglassSimpleMediumIcon } from "@phosphor-icons/react";
|
||||||
|
import { useAuth } from "../../hooks/useAuth";
|
||||||
import { Modal } from "../ui/Modal";
|
import { Modal } from "../ui/Modal";
|
||||||
|
|
||||||
interface PasskeyModalProps {
|
export function PasskeyModal() {
|
||||||
onUnlock: (password: string) => Promise<void>;
|
const { unlock } = useAuth();
|
||||||
}
|
|
||||||
|
|
||||||
export function PasskeyModal({ onUnlock }: PasskeyModalProps) {
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={true}>
|
<Modal isOpen={true}>
|
||||||
<LockKeyIcon
|
<HourglassSimpleMediumIcon
|
||||||
size={48}
|
size={48}
|
||||||
className="text-primary mx-auto mb-8 animate-pulse"
|
className="text-primary mx-auto mb-8 animate-pulse"
|
||||||
|
weight="duotone"
|
||||||
/>
|
/>
|
||||||
<h3 className="font-bold text-lg font-display text-primary">
|
<h3 className="font-bold text-lg font-display text-primary">
|
||||||
Authentication Required
|
You've been away a while.
|
||||||
</h3>
|
</h3>
|
||||||
<p className="py-4 font-sans">
|
<p className="py-4 font-sans">
|
||||||
We need your passkey to open your letters
|
Your letters are still there. Just need the key once more.
|
||||||
</p>
|
</p>
|
||||||
<div className="divider w-1/2 mx-auto text-xs text-neutral-content/30 mt-0"></div>
|
<div className="divider w-1/2 mx-auto text-xs text-neutral-content/30 mt-0"></div>
|
||||||
<p className="text-xs text-neutral-content/30 font-mono italic">
|
<p className="text-xs text-neutral-content/30 font-mono italic">
|
||||||
Your passkey is used to decrypt your data locally.
|
Nothing was lost.
|
||||||
</p>
|
</p>
|
||||||
<div className="modal-action items-center gap-4">
|
<div className="modal-action items-center gap-4">
|
||||||
<form
|
<form
|
||||||
@@ -30,7 +30,7 @@ export function PasskeyModal({ onUnlock }: PasskeyModalProps) {
|
|||||||
const formData = new FormData(e.currentTarget);
|
const formData = new FormData(e.currentTarget);
|
||||||
const password = formData.get("password") as string;
|
const password = formData.get("password") as string;
|
||||||
if (!password) return;
|
if (!password) return;
|
||||||
await onUnlock(password);
|
await unlock(password);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { mockUser } from "../../test/fixtures/user.fixture";
|
|||||||
import { server } from "../../test/mocks/server";
|
import { server } from "../../test/mocks/server";
|
||||||
import { useAuthStore } from "../store/useAuthStore";
|
import { useAuthStore } from "../store/useAuthStore";
|
||||||
import { useKeyStore } from "../store/useKeyStore";
|
import { useKeyStore } from "../store/useKeyStore";
|
||||||
|
import { CryptoUtils } from "../utils/crypto";
|
||||||
import {
|
import {
|
||||||
clearMasterKey,
|
clearMasterKey,
|
||||||
loadMasterKey,
|
loadMasterKey,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
import { useAuth } from "./useAuth";
|
import { useAuth } from "./useAuth";
|
||||||
|
|
||||||
vi.mock("../utils/keystore");
|
vi.mock("../utils/keystore");
|
||||||
|
vi.mock("../utils/crypto");
|
||||||
|
|
||||||
const VITE_API_URL = "http://piku-server";
|
const VITE_API_URL = "http://piku-server";
|
||||||
|
|
||||||
@@ -30,6 +32,11 @@ beforeEach(() => {
|
|||||||
isInitializing: true,
|
isInitializing: true,
|
||||||
});
|
});
|
||||||
useKeyStore.setState({ masterKey: null });
|
useKeyStore.setState({ masterKey: null });
|
||||||
|
|
||||||
|
vi.mocked(CryptoUtils.deriveKeyBundle).mockResolvedValue({
|
||||||
|
masterKey: mockMasterKey,
|
||||||
|
authHash: "mock-hash",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("isAuthenticated", () => {
|
describe("isAuthenticated", () => {
|
||||||
@@ -201,3 +208,68 @@ describe("initialize", () => {
|
|||||||
expect(useKeyStore.getState().masterKey).not.toBeNull();
|
expect(useKeyStore.getState().masterKey).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("unlock", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useAuthStore.setState({
|
||||||
|
accessToken: "valid-token",
|
||||||
|
user: mockUser,
|
||||||
|
isInitializing: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should derive the master key from the user password, validate it via API, and persist it", async () => {
|
||||||
|
let loginCalled = false;
|
||||||
|
server.use(
|
||||||
|
http.post(`${VITE_API_URL}/api/auth/login/`, async () => {
|
||||||
|
loginCalled = true;
|
||||||
|
return HttpResponse.json({ access: "token", user: mockUser });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const { result } = renderHook(() => useAuth());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.unlock("password");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(CryptoUtils.deriveKeyBundle).toHaveBeenCalledWith(
|
||||||
|
"password",
|
||||||
|
mockUser.email,
|
||||||
|
);
|
||||||
|
expect(loginCalled).toBe(true);
|
||||||
|
expect(saveMasterKey).toHaveBeenCalledWith(mockMasterKey);
|
||||||
|
expect(useKeyStore.getState().masterKey).toEqual(mockMasterKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should logout if user is not present", async () => {
|
||||||
|
useAuthStore.setState({ user: null });
|
||||||
|
const { result } = renderHook(() => useAuth());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.unlock("password");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(CryptoUtils.deriveKeyBundle).not.toHaveBeenCalled();
|
||||||
|
expect(saveMasterKey).not.toHaveBeenCalled();
|
||||||
|
expect(useAuthStore.getState().accessToken).toBeNull();
|
||||||
|
expect(clearMasterKey).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error and not persist the key if validation fails", async () => {
|
||||||
|
server.use(
|
||||||
|
http.post(
|
||||||
|
`${VITE_API_URL}/api/auth/login/`,
|
||||||
|
() => new HttpResponse(null, { status: 400 }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAuth());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await expect(result.current.unlock("wrong-password")).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(saveMasterKey).not.toHaveBeenCalled();
|
||||||
|
expect(useKeyStore.getState().masterKey).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ export const useAuth = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// try session refresh
|
|
||||||
const { data: refreshData } = await publicApi.post(endpoints.REFRESH);
|
const { data: refreshData } = await publicApi.post(endpoints.REFRESH);
|
||||||
const { data: userData } = await api.get(endpoints.ME, {
|
const { data: userData } = await api.get(endpoints.ME, {
|
||||||
headers: { Authorization: `Bearer ${refreshData.access}` },
|
headers: { Authorization: `Bearer ${refreshData.access}` },
|
||||||
@@ -71,16 +70,24 @@ export const useAuth = () => {
|
|||||||
}, [setMasterKey]);
|
}, [setMasterKey]);
|
||||||
|
|
||||||
const unlock = async (password: string) => {
|
const unlock = async (password: string) => {
|
||||||
if (!user) return;
|
if (!user) {
|
||||||
|
await logout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
const { masterKey, authHash } = await CryptoUtils.deriveKeyBundle(
|
||||||
const { masterKey } = await CryptoUtils.deriveKeyBundle(
|
|
||||||
password,
|
password,
|
||||||
user.email,
|
user.email,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Validate password by calling login endpoint
|
||||||
|
await api.post(endpoints.LOGIN, {
|
||||||
|
email: user.email,
|
||||||
|
password: authHash,
|
||||||
|
});
|
||||||
|
|
||||||
await saveMasterKey(masterKey);
|
await saveMasterKey(masterKey);
|
||||||
setMasterKey(masterKey);
|
setMasterKey(masterKey);
|
||||||
} catch {}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -75,7 +75,9 @@ describe("Drawer Page", () => {
|
|||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText(/Authentication Required/i)).toBeInTheDocument();
|
expect(
|
||||||
|
screen.getByText(/You've been away a while./i),
|
||||||
|
).toBeInTheDocument();
|
||||||
expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument();
|
expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from "../utils/dateFormat.ts";
|
} from "../utils/dateFormat.ts";
|
||||||
|
|
||||||
export default function Drawer() {
|
export default function Drawer() {
|
||||||
const { user, logout, unlock } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
|
||||||
const [openSection, setOpenSection] = useState<string | null>(null);
|
const [openSection, setOpenSection] = useState<string | null>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -30,7 +30,7 @@ export default function Drawer() {
|
|||||||
<div className="min-h-screen w-full bg-base-100 text-base-content flex flex-col items-center py-12 px-5 pb-32 font-serif transition-colors">
|
<div className="min-h-screen w-full bg-base-100 text-base-content flex flex-col items-center py-12 px-5 pb-32 font-serif transition-colors">
|
||||||
<div className="fixed inset-0 bg-vig pointer-events-none z-0" />
|
<div className="fixed inset-0 bg-vig pointer-events-none z-0" />
|
||||||
|
|
||||||
{isAuthRequired && <PasskeyModal onUnlock={unlock} />}
|
{isAuthRequired && <PasskeyModal />}
|
||||||
<header className="text-center mb-12 z-10 animate-in fade-in slide-in-from-top-4 duration-500">
|
<header className="text-center mb-12 z-10 animate-in fade-in slide-in-from-top-4 duration-500">
|
||||||
<Logo />
|
<Logo />
|
||||||
<div className="font-sans text-xs tracking-widester uppercase text-base-content/40 mt-2">
|
<div className="font-sans text-xs tracking-widester uppercase text-base-content/40 mt-2">
|
||||||
|
|||||||
+45
-44
@@ -1,9 +1,9 @@
|
|||||||
import { InfoIcon } from "@phosphor-icons/react";
|
import { InfoIcon } from "@phosphor-icons/react";
|
||||||
|
import { ReactLenis } from "lenis/react";
|
||||||
import {
|
import {
|
||||||
motion,
|
motion,
|
||||||
useMotionValueEvent,
|
useMotionValueEvent,
|
||||||
useScroll,
|
useScroll,
|
||||||
useSpring,
|
|
||||||
useTransform,
|
useTransform,
|
||||||
} from "motion/react";
|
} from "motion/react";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
@@ -16,14 +16,9 @@ import { formatDate } from "../utils/dateFormat.ts";
|
|||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const sectionContainer1 = useRef<HTMLDivElement>(null);
|
const sectionContainer1 = useRef<HTMLDivElement>(null);
|
||||||
const { scrollYProgress: section1ScrollProgress } = useScroll({
|
const { scrollYProgress } = useScroll({
|
||||||
target: sectionContainer1,
|
target: sectionContainer1,
|
||||||
});
|
});
|
||||||
const smoothProgress1 = useSpring(section1ScrollProgress, {
|
|
||||||
stiffness: 100,
|
|
||||||
damping: 30,
|
|
||||||
restDelta: 0.001,
|
|
||||||
});
|
|
||||||
const [isEnvelopeFlipped, setIsEnvelopeFlipped] = useState(true);
|
const [isEnvelopeFlipped, setIsEnvelopeFlipped] = useState(true);
|
||||||
const [flapOpen, setFlapOpen] = useState(false);
|
const [flapOpen, setFlapOpen] = useState(false);
|
||||||
const [recipient, setRecipient] = useState("someone dear");
|
const [recipient, setRecipient] = useState("someone dear");
|
||||||
@@ -31,7 +26,7 @@ export default function Home() {
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useMotionValueEvent(section1ScrollProgress, "change", (latestScrollValue) => {
|
useMotionValueEvent(scrollYProgress, "change", (latestScrollValue) => {
|
||||||
if (latestScrollValue > 0.54) {
|
if (latestScrollValue > 0.54) {
|
||||||
setFlapOpen(false);
|
setFlapOpen(false);
|
||||||
} else {
|
} else {
|
||||||
@@ -55,6 +50,7 @@ export default function Home() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ReactLenis root options={{ lerp: 0.1, duration: 1.5, smoothWheel: true }}>
|
||||||
<section
|
<section
|
||||||
ref={sectionContainer1}
|
ref={sectionContainer1}
|
||||||
className="relative w-full h-[850vh] bg-base-100 font-serif"
|
className="relative w-full h-[850vh] bg-base-100 font-serif"
|
||||||
@@ -64,8 +60,8 @@ export default function Home() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
className="absolute flex flex-col items-center justify-center pointer-events-none"
|
className="absolute flex flex-col items-center justify-center pointer-events-none"
|
||||||
style={{
|
style={{
|
||||||
opacity: useTransform(smoothProgress1, [0, 0.12, 1], [1, 0, 0]),
|
opacity: useTransform(scrollYProgress, [0, 0.12, 1], [1, 0, 0]),
|
||||||
scale: useTransform(smoothProgress1, [0, 0.12], [1, 10]),
|
scale: useTransform(scrollYProgress, [0, 0.12], [1, 10]),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h1 className="text-neutral-content/40 text-4xl md:text-6xl text-center px-6">
|
<h1 className="text-neutral-content/40 text-4xl md:text-6xl text-center px-6">
|
||||||
@@ -79,9 +75,9 @@ export default function Home() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
className="absolute text-center"
|
className="absolute text-center"
|
||||||
style={{
|
style={{
|
||||||
opacity: useTransform(smoothProgress1, [0, 0.15, 0.2], [0, 1, 0]),
|
opacity: useTransform(scrollYProgress, [0, 0.15, 0.2], [0, 1, 0]),
|
||||||
y: useTransform(smoothProgress1, [0, 0.15, 0.2], [40, 0, -40]),
|
y: useTransform(scrollYProgress, [0, 0.15, 0.2], [40, 0, -40]),
|
||||||
scale: useTransform(smoothProgress1, [0, 0.15, 0.2], [0.8, 1, 3]),
|
scale: useTransform(scrollYProgress, [0, 0.15, 0.2], [0.8, 1, 3]),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="mt-6 text-4xl md:text-6xl text-base-content/60 italic">
|
<div className="mt-6 text-4xl md:text-6xl text-base-content/60 italic">
|
||||||
@@ -93,11 +89,11 @@ export default function Home() {
|
|||||||
className="absolute text-center px-6"
|
className="absolute text-center px-6"
|
||||||
style={{
|
style={{
|
||||||
opacity: useTransform(
|
opacity: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.18, 0.25, 0.3],
|
[0.18, 0.25, 0.3],
|
||||||
[0, 1, 0],
|
[0, 1, 0],
|
||||||
),
|
),
|
||||||
y: useTransform(smoothProgress1, [0.18, 0.25, 0.3], [20, 0, -20]),
|
y: useTransform(scrollYProgress, [0.18, 0.25, 0.3], [20, 0, -20]),
|
||||||
}}
|
}}
|
||||||
transition={{ delay: 4 }}
|
transition={{ delay: 4 }}
|
||||||
>
|
>
|
||||||
@@ -106,12 +102,12 @@ export default function Home() {
|
|||||||
className="mt-6 text-4xl md:text-6xl text-base-content/60 "
|
className="mt-6 text-4xl md:text-6xl text-base-content/60 "
|
||||||
style={{
|
style={{
|
||||||
opacity: useTransform(
|
opacity: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.22, 0.25, 0.35, 0.4],
|
[0.22, 0.25, 0.35, 0.4],
|
||||||
[0, 1, 1, 0],
|
[0, 1, 1, 0],
|
||||||
),
|
),
|
||||||
y: useTransform(
|
y: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.25, 0.3, 0.35, 0.4],
|
[0.25, 0.3, 0.35, 0.4],
|
||||||
[20, 0, 0, -20],
|
[20, 0, 0, -20],
|
||||||
),
|
),
|
||||||
@@ -137,12 +133,12 @@ export default function Home() {
|
|||||||
<motion.h2
|
<motion.h2
|
||||||
style={{
|
style={{
|
||||||
opacity: useTransform(
|
opacity: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.3, 0.35, 0.4, 0.45],
|
[0.3, 0.35, 0.4, 0.45],
|
||||||
[0, 1, 1, 0],
|
[0, 1, 1, 0],
|
||||||
),
|
),
|
||||||
y: useTransform(
|
y: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.3, 0.35, 0.4, 0.45],
|
[0.3, 0.35, 0.4, 0.45],
|
||||||
[40, 0, 0, -40],
|
[40, 0, 0, -40],
|
||||||
),
|
),
|
||||||
@@ -159,12 +155,12 @@ export default function Home() {
|
|||||||
<motion.h2
|
<motion.h2
|
||||||
style={{
|
style={{
|
||||||
opacity: useTransform(
|
opacity: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.45, 0.5, 0.55, 0.6],
|
[0.45, 0.5, 0.55, 0.6],
|
||||||
[0, 1, 1, 0],
|
[0, 1, 1, 0],
|
||||||
),
|
),
|
||||||
y: useTransform(
|
y: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.45, 0.5, 0.55, 0.6],
|
[0.45, 0.5, 0.55, 0.6],
|
||||||
[40, 0, 0, -40],
|
[40, 0, 0, -40],
|
||||||
),
|
),
|
||||||
@@ -185,12 +181,12 @@ export default function Home() {
|
|||||||
<motion.h2
|
<motion.h2
|
||||||
style={{
|
style={{
|
||||||
opacity: useTransform(
|
opacity: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.6, 0.63, 0.72, 0.75],
|
[0.6, 0.63, 0.72, 0.75],
|
||||||
[0, 1, 1, 0],
|
[0, 1, 1, 0],
|
||||||
),
|
),
|
||||||
y: useTransform(
|
y: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.6, 0.63, 0.72, 0.75],
|
[0.6, 0.63, 0.72, 0.75],
|
||||||
[40, 0, 0, -40],
|
[40, 0, 0, -40],
|
||||||
),
|
),
|
||||||
@@ -202,7 +198,7 @@ export default function Home() {
|
|||||||
className="font-display text-accent"
|
className="font-display text-accent"
|
||||||
style={{
|
style={{
|
||||||
color: useTransform(
|
color: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.67, 1],
|
[0.67, 1],
|
||||||
["var(--color-accent)", "var(--color-neutral)"],
|
["var(--color-accent)", "var(--color-neutral)"],
|
||||||
),
|
),
|
||||||
@@ -212,14 +208,14 @@ export default function Home() {
|
|||||||
</motion.span>
|
</motion.span>
|
||||||
<motion.span
|
<motion.span
|
||||||
style={{
|
style={{
|
||||||
opacity: useTransform(smoothProgress1, [0.66, 0.7], [0, 1]),
|
opacity: useTransform(scrollYProgress, [0.66, 0.7], [0, 1]),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<motion.span
|
<motion.span
|
||||||
className="font-display text-accent"
|
className="font-display text-accent"
|
||||||
style={{
|
style={{
|
||||||
color: useTransform(
|
color: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.67, 1],
|
[0.67, 1],
|
||||||
["var(--color-accent)", "var(--color-neutral)"],
|
["var(--color-accent)", "var(--color-neutral)"],
|
||||||
),
|
),
|
||||||
@@ -238,20 +234,20 @@ export default function Home() {
|
|||||||
<motion.h2
|
<motion.h2
|
||||||
style={{
|
style={{
|
||||||
opacity: useTransform(
|
opacity: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.75, 0.8, 0.85, 0.9],
|
[0.75, 0.8, 0.85, 0.9],
|
||||||
[0, 1, 1, 0],
|
[0, 1, 1, 0],
|
||||||
),
|
),
|
||||||
y: useTransform(
|
y: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.75, 0.8, 0.85, 0.9],
|
[0.75, 0.8, 0.85, 0.9],
|
||||||
[40, 0, 0, -40],
|
[40, 0, 0, -40],
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
|
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
|
||||||
>
|
>
|
||||||
and even <span className="font-display text-error">burn it</span> to
|
and even <span className="font-display text-error">burn it</span>{" "}
|
||||||
release the burden.
|
to release the burden.
|
||||||
</motion.h2>
|
</motion.h2>
|
||||||
{/* Outro */}
|
{/* Outro */}
|
||||||
<motion.h2
|
<motion.h2
|
||||||
@@ -259,8 +255,8 @@ export default function Home() {
|
|||||||
"italic absolute text-4xl md:text-6xl text-center px-10 leading-tight"
|
"italic absolute text-4xl md:text-6xl text-center px-10 leading-tight"
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
opacity: useTransform(smoothProgress1, [0.9, 1], [0, 1]),
|
opacity: useTransform(scrollYProgress, [0.9, 1], [0, 1]),
|
||||||
y: useTransform(smoothProgress1, [0.9, 1], [80, 0]),
|
y: useTransform(scrollYProgress, [0.9, 1], [80, 0]),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
You've been carrying it long enough.
|
You've been carrying it long enough.
|
||||||
@@ -271,10 +267,10 @@ export default function Home() {
|
|||||||
"z-100 absolute -bottom-12 md:bottom-0 font-display flex flex-wrap md:flex-nowrap gap-4 md:gap-12 justify-center"
|
"z-100 absolute -bottom-12 md:bottom-0 font-display flex flex-wrap md:flex-nowrap gap-4 md:gap-12 justify-center"
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
opacity: useTransform(smoothProgress1, [0.98, 1], [0, 1]),
|
opacity: useTransform(scrollYProgress, [0.98, 1], [0, 1]),
|
||||||
y: useTransform(smoothProgress1, [0.98, 1], [80, 0]),
|
y: useTransform(scrollYProgress, [0.98, 1], [80, 0]),
|
||||||
display: useTransform(
|
display: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.96, 1],
|
[0.96, 1],
|
||||||
["none", "flex"],
|
["none", "flex"],
|
||||||
),
|
),
|
||||||
@@ -307,13 +303,17 @@ export default function Home() {
|
|||||||
className={"z-21 absolute"}
|
className={"z-21 absolute"}
|
||||||
style={{
|
style={{
|
||||||
opacity: useTransform(
|
opacity: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.3, 0.4, 0.5, 0.52],
|
[0.3, 0.4, 0.5, 0.52],
|
||||||
[0, 1, 0.1, 0],
|
[0, 1, 0.1, 0],
|
||||||
),
|
),
|
||||||
y: useTransform(smoothProgress1, [0.3, 0.45, 0.5], [300, 0, 200]),
|
y: useTransform(
|
||||||
|
scrollYProgress,
|
||||||
|
[0.3, 0.45, 0.5],
|
||||||
|
[300, 0, 200],
|
||||||
|
),
|
||||||
scale: useTransform(
|
scale: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.3, 0.4, 0.5],
|
[0.3, 0.4, 0.5],
|
||||||
[1, 1, 0.6],
|
[1, 1, 0.6],
|
||||||
),
|
),
|
||||||
@@ -331,11 +331,11 @@ export default function Home() {
|
|||||||
className="absolute scale-50 md:scale-80 z-10"
|
className="absolute scale-50 md:scale-80 z-10"
|
||||||
style={{
|
style={{
|
||||||
opacity: useTransform(
|
opacity: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.4, 0.45, 0.5, 0.7, 0.9, 1],
|
[0.4, 0.45, 0.5, 0.7, 0.9, 1],
|
||||||
[0, 0.6, 1, 1, 0.3, 0],
|
[0, 0.6, 1, 1, 0.3, 0],
|
||||||
),
|
),
|
||||||
y: useTransform(smoothProgress1, [0.45, 0.5, 1], [600, 200, 0]),
|
y: useTransform(scrollYProgress, [0.45, 0.5, 1], [600, 200, 0]),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EnvelopeReveal
|
<EnvelopeReveal
|
||||||
@@ -353,11 +353,11 @@ export default function Home() {
|
|||||||
className="fixed bottom-0 z-10 font-sans -mb-6 scale-85 md:scale-100 md:mb-0"
|
className="fixed bottom-0 z-10 font-sans -mb-6 scale-85 md:scale-100 md:mb-0"
|
||||||
style={{
|
style={{
|
||||||
opacity: useTransform(
|
opacity: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.98, 0.995, 1],
|
[0.98, 0.995, 1],
|
||||||
[0, 0.5, 1],
|
[0, 0.5, 1],
|
||||||
),
|
),
|
||||||
y: useTransform(smoothProgress1, [0.98, 1], [50, -10]),
|
y: useTransform(scrollYProgress, [0.98, 1], [50, -10]),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Saajan
|
<Saajan
|
||||||
@@ -375,7 +375,7 @@ export default function Home() {
|
|||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: useTransform(
|
backgroundColor: useTransform(
|
||||||
smoothProgress1,
|
scrollYProgress,
|
||||||
[0.45, 0.5, 0.7, 0.75, 1],
|
[0.45, 0.5, 0.7, 0.75, 1],
|
||||||
[
|
[
|
||||||
"var(--color-primary)",
|
"var(--color-primary)",
|
||||||
@@ -385,12 +385,13 @@ export default function Home() {
|
|||||||
"var(--color-error)",
|
"var(--color-error)",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
scale: useTransform(smoothProgress1, [0, 1], [0.6, 2.5]),
|
scale: useTransform(scrollYProgress, [0, 1], [0.6, 2.5]),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="absolute border border-primary/5 w-64 h-64 rounded-full backdrop-blur-[1px]" />
|
<div className="absolute border border-primary/5 w-64 h-64 rounded-full backdrop-blur-[1px]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</ReactLenis>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,18 @@ until $CONTAINER_BIN exec "$DB_NAME" pg_isready -U "${DB_USER:-test}" >/dev/null
|
|||||||
done
|
done
|
||||||
|
|
||||||
export PIKU_ENV_FILE="$ENV_FILE"
|
export PIKU_ENV_FILE="$ENV_FILE"
|
||||||
|
|
||||||
|
# NOTE: When running in Gitea Actions (within container), We must ponint DB and mail to the internal docker host instead.
|
||||||
|
if [ "$GITEA_ACTIONS" = "true" ]; then
|
||||||
|
sudo apt-get update && sudo apt-get install -y iproute2
|
||||||
|
# Sample: "default via <internal docker host IP> dev <network interface> proto dhcp src <IP> metric 100"
|
||||||
|
HOST_IP=$(ip route show default | awk '/default/ {print $3}')
|
||||||
|
echo "Running on Gitea. Internal Docker host... $HOST_IP"
|
||||||
|
|
||||||
|
export DB_HOST=$HOST_IP
|
||||||
|
export EMAIL_HOST=$HOST_IP
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Starting Backend..."
|
echo "Starting Backend..."
|
||||||
mkdir -p ./tmp/logs
|
mkdir -p ./tmp/logs
|
||||||
(
|
(
|
||||||
|
|||||||
Reference in New Issue
Block a user