diff --git a/.env.example b/.env.example deleted file mode 100644 index 2bb19b6..0000000 --- a/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -VITE_SUPABASE_URL=http://127.0.0.1:54321 -VITE_SUPABASE_ANON_KEY= diff --git a/package-lock.json b/package-lock.json index 70e1702..e60b13e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,13 @@ "name": "ncu-app", "version": "0.0.0", "dependencies": { + "@heroicons/react": "^2.1.5", "@supabase/supabase-js": "^2.45.3", "@tanstack/react-router": "^1.56.2", + "flowbite-react-icons": "^1.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "supabase": "^1.200.3" + "supabase": "^1.223.10" }, "devDependencies": { "@playwright/test": "^1.47.0", @@ -2328,6 +2330,15 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@heroicons/react": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.5.tgz", + "integrity": "sha512-FuzFN+BsHa+7OxbvAERtgBTNeZpUjgM/MIizfVkSCL2/edriN0Hx/DWRCR//aPYwO5QX/YlgLGXk+E3PcfZwjA==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -3804,18 +3815,19 @@ ] }, "node_modules/bin-links": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-4.0.4.tgz", - "integrity": "sha512-cMtq4W5ZsEwcutJrVId+a/tjt8GSbS+h0oNkdl6+6rBuEv8Ot33Bevj5KPm40t309zuhVic8NjpuL42QCiJWWA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-5.0.0.tgz", + "integrity": "sha512-sdleLVfCjBtgO5cNjA2HVRvWBJAHs4zwenaCPMNJAJU0yNxpzj80IpjOIimkpkr+mhlA+how5poQtt53PygbHA==", "license": "ISC", "dependencies": { - "cmd-shim": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0", - "read-cmd-shim": "^4.0.0", - "write-file-atomic": "^5.0.0" + "cmd-shim": "^7.0.0", + "npm-normalize-package-bin": "^4.0.0", + "proc-log": "^5.0.0", + "read-cmd-shim": "^5.0.0", + "write-file-atomic": "^6.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/binary-extensions": { @@ -4058,12 +4070,12 @@ "dev": true }, "node_modules/cmd-shim": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-6.0.3.tgz", - "integrity": "sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-7.0.0.tgz", + "integrity": "sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw==", "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/color": { @@ -5192,6 +5204,16 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/flowbite-react-icons": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/flowbite-react-icons/-/flowbite-react-icons-1.1.0.tgz", + "integrity": "sha512-wCBIStVygQ+CQlErY7s28vbXBRz3d6iqaSEdkSjtZb66lrV0uDs2NfUA2mWuIVhHUEC/CDktyUL0DG3J2gt/WQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -6641,12 +6663,12 @@ } }, "node_modules/npm-normalize-package-bin": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", - "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/object-assign": { @@ -7173,6 +7195,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -7293,12 +7324,12 @@ } }, "node_modules/read-cmd-shim": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-4.0.0.tgz", - "integrity": "sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-5.0.0.tgz", + "integrity": "sha512-SEbJV7tohp3DAAILbEMPXavBjAnMN0tVnh4+9G8ihV4Pq3HYF9h8QNez9zkJ1ILkv9G2BjdzwctznGZXgu/HGw==", "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/readable-stream": { @@ -8126,13 +8157,13 @@ } }, "node_modules/supabase": { - "version": "1.200.3", - "resolved": "https://registry.npmjs.org/supabase/-/supabase-1.200.3.tgz", - "integrity": "sha512-3NdhqBkfPVlm+rAhWQoVcyr54kykuAlHav/GWaAoQEHBDbbYI1lhbDzugk8ryQg92vSLwr3pWz0s4Hjdte8WyQ==", + "version": "1.223.10", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-1.223.10.tgz", + "integrity": "sha512-a5Wi562n0eiV3w359qiCjewyVad688Z3+JHdvLybdlITrwvNIcR6QYqRR6EzjKY5V/sNCqC+5sNf40wDYAYcHg==", "hasInstallScript": true, "license": "MIT", "dependencies": { - "bin-links": "^4.0.3", + "bin-links": "^5.0.0", "https-proxy-agent": "^7.0.2", "node-fetch": "^3.3.2", "tar": "7.4.3" @@ -9779,16 +9810,16 @@ "dev": true }, "node_modules/write-file-atomic": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-6.0.0.tgz", + "integrity": "sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==", "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/ws": { diff --git a/package.json b/package.json index 1afabaf..bd824cf 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,13 @@ "test": "playwright test" }, "dependencies": { + "@heroicons/react": "^2.1.5", "@supabase/supabase-js": "^2.45.3", "@tanstack/react-router": "^1.56.2", + "flowbite-react-icons": "^1.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "supabase": "^1.200.3" + "supabase": "^1.223.10" }, "devDependencies": { "@playwright/test": "^1.47.0", diff --git a/src/components/DrawerOption.tsx b/src/components/DrawerOption.tsx index a3c6fe1..0fbb027 100644 --- a/src/components/DrawerOption.tsx +++ b/src/components/DrawerOption.tsx @@ -1,6 +1,7 @@ import { Link } from "@tanstack/react-router"; +import { CaretDown, CaretRight } from "flowbite-react-icons/solid"; import { useState } from "react"; -import { HStack, SidebarArrowDownIcon, SidebarArrowRightIcon, VStack } from "../components"; +import { HStack, VStack } from "../components"; const options = [ { name: "首頁", engName: "Home", pageNav: "/" }, @@ -29,9 +30,9 @@ export const DrawerOption = () => {
toggleOption(option.name)}> {openOptions[option.name] ? ( - + ) : ( - + )}
diff --git a/src/components/icons/ClockIcon.tsx b/src/components/icons/ClockIcon.tsx new file mode 100644 index 0000000..feb0e0a --- /dev/null +++ b/src/components/icons/ClockIcon.tsx @@ -0,0 +1,12 @@ + +import React from 'react'; +import IconProps from '../interface/IconProps'; +import { BasicIcon } from './BasicIcon'; + +export const ClockIcon: React.FC = ({ fill, stroke, size }) => ( + + + +); + +export default ClockIcon; diff --git a/src/components/icons/PinIcon.tsx b/src/components/icons/PinIcon.tsx new file mode 100644 index 0000000..4fdd14c --- /dev/null +++ b/src/components/icons/PinIcon.tsx @@ -0,0 +1,12 @@ + +import React from 'react'; +import IconProps from '../interface/IconProps'; +import { BasicIcon } from './BasicIcon'; + +export const PinIcon: React.FC = ({ fill, stroke, size }) => ( + + + +); + +export default PinIcon; diff --git a/src/components/index.ts b/src/components/index.ts index 44eef03..0391eae 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -7,7 +7,9 @@ export { Header } from './Header'; // Icons export { BellIcon } from './icons/BellIcon'; export { CalendarIcon } from './icons/CalendarIcon'; +export { ClockIcon } from './icons/ClockIcon'; export { LogoutIcon } from './icons/LogoutIcon'; +export { PinIcon } from './icons/PinIcon'; export { PlusIcon } from './icons/PlusIcon'; export { SidebarArrowDownIcon } from './icons/SidebarArrowDownIcon'; export { SidebarArrowRightIcon } from './icons/SidebarArrowRightIcon'; diff --git a/src/routes/events/create.tsx b/src/routes/events/create.tsx index 0fa6ae3..7eced56 100644 --- a/src/routes/events/create.tsx +++ b/src/routes/events/create.tsx @@ -35,6 +35,11 @@ function CreateEventScreen() { const [preview, setPreview] = useState() const [inputs, setInputs] = useState({ name: '', + start_time: '', + end_time: '', + location: '', + fee: 0, + description: '' }) // create a preview as a side effect, whenever selected file is changed @@ -106,6 +111,8 @@ function CreateEventScreen() { type="datetime-local" id="start-time" name="start-time" + value={inputs.start_time} + onChange={(text) => { setInputs({ ...inputs, start_time: text.target.value }) }} />
@@ -114,6 +121,8 @@ function CreateEventScreen() { type="datetime-local" id="end-time" name="end-time" + value={inputs.end_time} + onChange={(text) => { setInputs({ ...inputs, end_time: text.target.value }) }} />

活動地點

@@ -121,18 +130,24 @@ function CreateEventScreen() { style={styles.input} className="rounded" placeholder="請輸入活動地點" + value={inputs.location} + onChange={(text) => { setInputs({ ...inputs, location: text.target.value }) }} />

參加費用

{ setInputs({ ...inputs, fee: Number(text.target.value) }) }} />

活動介紹

{ setInputs({ ...inputs, description: text.target.value }) }} />
diff --git a/src/routes/events/index.tsx b/src/routes/events/index.tsx index 6909804..b834045 100644 --- a/src/routes/events/index.tsx +++ b/src/routes/events/index.tsx @@ -1,14 +1,21 @@ -import { createFileRoute, Link } from '@tanstack/react-router'; -import { BellIcon, Header, PlusIcon } from '../../components'; +import { createFileRoute } from '@tanstack/react-router'; +import { Clock } from "flowbite-react-icons/solid"; +import { BellIcon, Header, PinIcon, PlusIcon } from '../../components'; import { AuthGuard } from '../../utils/auth'; import { supabase } from '../../utils/supabase'; +interface Event { + created_at: string; + description: string | null; + end_time: string | null; + fee: number | null; + id: number; + name: string | null; + start_time: string | null; + type: number | null; + user_id: string; + location: string | null; +} -const styles = { - container: { - flex: 1, - backgroundColor: '#333333', - }, -}; export const Route = createFileRoute('/events/')({ beforeLoad: AuthGuard, loader: async () => { @@ -19,7 +26,6 @@ export const Route = createFileRoute('/events/')({ if (error !== null) { throw error } - return { events: data } }, component: EventIndex @@ -28,23 +34,41 @@ export const Route = createFileRoute('/events/')({ function EventIndex() { const { events } = Route.useLoaderData() const navigate = Route.useNavigate(); + + // console.log(events[0].start_time) + return ( <>
-
-

活動列表

- { - events.map((event) => ( - {event.name} - )) - } +
+

最新揪人

+
+
+ {events.map((event) => ( + + ))} +
+
+ +

最新活動

+
+
+ {events.map((event) => ( + + ))} +
+
- +
{/* This button will close the dialog */} @@ -67,3 +96,28 @@ function EventIndex() { ) } + +function EventCard({ event }: { event: Event }) { + const startTime = event.start_time ? new Date(event.start_time) : new Date(); + return ( +
+
+
+

{event.name}

+

+ + {startTime.toLocaleString('zh-TW', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + })} +

+

+ + {event.location || '位置未提供'} +

+
+
+ ); +} diff --git a/src/routes/login.tsx b/src/routes/login.tsx index f30a8c4..e108cc5 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -1,7 +1,6 @@ -import { createFileRoute } from '@tanstack/react-router' -import { useState } from 'react' -import { VStack } from '../components' -import { supabase } from '../utils/supabase' +import { createFileRoute } from '@tanstack/react-router'; +import { useState } from 'react'; +import { supabase } from '../utils/supabase'; export const Route = createFileRoute('/login')({ component: LoginPage, @@ -15,16 +14,17 @@ export const Route = createFileRoute('/login')({ function LoginPage() { const [email, setEmail] = useState('') const [password, setPassword] = useState('') + const [isRemember, setIsRemember] = useState(false); + const [isError, setIsError] = useState(false); const { redirect: redirectUrl } = Route.useSearch() - async function login() { - const { data: { session }, error } = await supabase.auth.signInWithPassword({ - email, - password - }) + async function login(event: React.FormEvent) { + event.preventDefault(); + const { data: { session }, error } = await supabase.auth.signInWithPassword({ email, password }); if (error !== null) { - throw error + setPassword(''); + setIsError(true); } if (session !== null) { @@ -33,19 +33,68 @@ function LoginPage() { } return ( -
- -
- - { setEmail(e.target.value) }} /> -
-
- - { setPassword(e.target.value) }} /> -
-
- -
+ + setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> +
+ {/* The style of the checkbox is refered from https://www.material-tailwind.com/docs/html/checkbox */} + + +
+ { + isError + ? ( + + {isError ? Labels.wrongAccountOrPassword : ''} + + ) + : <> + } + + ) +} + +class Labels { + private constructor() { } + static readonly account = '信箱或手機號碼'; + static readonly password = '密碼'; + static readonly remember = '保持我的登入狀態'; + static readonly login = '登入'; + static readonly wrongAccountOrPassword = '帳號或密碼錯誤'; } diff --git a/src/utils/database.types.ts b/src/utils/database.types.ts index c199ae4..c8e5726 100644 --- a/src/utils/database.types.ts +++ b/src/utils/database.types.ts @@ -56,6 +56,7 @@ export type Database = { end_time: string | null fee: number | null id: number + location: string | null name: string | null start_time: string | null type: number | null @@ -67,6 +68,7 @@ export type Database = { end_time?: string | null fee?: number | null id?: number + location?: string | null name?: string | null start_time?: string | null type?: number | null @@ -78,6 +80,7 @@ export type Database = { end_time?: string | null fee?: number | null id?: number + location?: string | null name?: string | null start_time?: string | null type?: number | null diff --git a/supabase/migrations/20241114141253_remote_schema.sql b/supabase/migrations/20241114141253_remote_schema.sql new file mode 100644 index 0000000..36989d5 --- /dev/null +++ b/supabase/migrations/20241114141253_remote_schema.sql @@ -0,0 +1,3 @@ +alter table "public"."events" add column "location" text; + +