├── .editorconfig
├── .eslintrc.cjs
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── panda.config.ts
├── postcss.config.cjs
├── public
├── favicon.ico
└── fe2024_og.png
├── src
├── app
│ ├── components
│ │ ├── ChildcareSection
│ │ │ ├── ChildcareSection.tsx
│ │ │ ├── assets
│ │ │ │ └── jaranda.png
│ │ │ └── index.ts
│ │ ├── CoCSection
│ │ │ ├── CoCSection.tsx
│ │ │ └── index.ts
│ │ ├── ContactSection
│ │ │ ├── ContactSection.tsx
│ │ │ └── index.ts
│ │ ├── FooterSection
│ │ │ ├── FooterSection.tsx
│ │ │ └── index.ts
│ │ ├── HeroIntroWrap
│ │ │ ├── HeroIntroWrap.tsx
│ │ │ └── index.ts
│ │ ├── HeroSection
│ │ │ ├── HeroSection.tsx
│ │ │ ├── components
│ │ │ │ ├── DataLocation.tsx
│ │ │ │ ├── HeroLogo.tsx
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ ├── IntroSection
│ │ │ ├── IntroSection.tsx
│ │ │ └── index.ts
│ │ ├── OpenSourceSection
│ │ │ ├── OpenSourceSection.tsx
│ │ │ ├── assets
│ │ │ │ ├── babel.png
│ │ │ │ ├── core-js.png
│ │ │ │ ├── ky.png
│ │ │ │ ├── pretendard.png
│ │ │ │ ├── query.png
│ │ │ │ ├── react-hook-form.png
│ │ │ │ ├── ts-pattern.png
│ │ │ │ └── zod.png
│ │ │ └── index.ts
│ │ ├── ProgramSection
│ │ │ ├── ProgramSection.tsx
│ │ │ ├── assets
│ │ │ │ └── timetable.jpg
│ │ │ ├── components
│ │ │ │ ├── ProgramHeader.tsx
│ │ │ │ ├── ProgramTab.tsx
│ │ │ │ ├── SessionList.tsx
│ │ │ │ ├── components
│ │ │ │ │ ├── ArrowIcon.tsx
│ │ │ │ │ ├── ClockIcon.tsx
│ │ │ │ │ ├── CloseIcon.tsx
│ │ │ │ │ ├── SessionModal.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ ├── SponsorSection
│ │ │ ├── SponsorSection.tsx
│ │ │ ├── assets
│ │ │ │ └── it.png
│ │ │ ├── comopnents
│ │ │ │ ├── SponsorInfo.tsx
│ │ │ │ ├── assets
│ │ │ │ │ ├── F-Lab.png
│ │ │ │ │ ├── carrot.png
│ │ │ │ │ ├── googlecloud.png
│ │ │ │ │ ├── imweb.png
│ │ │ │ │ ├── oliveyoung.png
│ │ │ │ │ ├── soomgo.png
│ │ │ │ │ ├── stibee.png
│ │ │ │ │ ├── toss.png
│ │ │ │ │ ├── wooritech.png
│ │ │ │ │ └── wyyyes.png
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.tsx
│ └── reset.css
├── features
│ ├── .gitkeep
│ ├── aurora
│ │ ├── assets
│ │ │ ├── background.png
│ │ │ ├── background2.png
│ │ │ ├── background3.png
│ │ │ ├── background4-half.png
│ │ │ ├── background4-min.png
│ │ │ ├── background4-tiny.png
│ │ │ ├── background4.png
│ │ │ └── bg2-tiny.png
│ │ ├── components
│ │ │ ├── Aurora.tsx
│ │ │ └── index.ts
│ │ └── contexts
│ │ │ ├── AuroraContext.tsx
│ │ │ └── index.ts
│ ├── effects
│ │ └── components
│ │ │ ├── AuroraContainer.tsx
│ │ │ ├── SphereContainer.tsx
│ │ │ ├── SphereEffect
│ │ │ ├── SphereEffect.tsx
│ │ │ ├── assets
│ │ │ │ └── img_noise.png
│ │ │ ├── components
│ │ │ │ ├── GradientSphere
│ │ │ │ │ ├── ExtendedMaterial.ts
│ │ │ │ │ ├── GradientSphere.tsx
│ │ │ │ │ ├── MeshPhysicalMaterialWithGlow.tsx
│ │ │ │ │ ├── ReactExtendedMaterial.tsx
│ │ │ │ │ ├── assets
│ │ │ │ │ │ ├── gradient1.jpg
│ │ │ │ │ │ ├── gradient2.jpg
│ │ │ │ │ │ └── gradient3.jpg
│ │ │ │ │ ├── external
│ │ │ │ │ │ ├── BlurShader.ts
│ │ │ │ │ │ ├── BokehPass2.js
│ │ │ │ │ │ ├── BokehShader.ts
│ │ │ │ │ │ ├── FixedNoiseEffect.ts
│ │ │ │ │ │ ├── KawaseBlurPass.js
│ │ │ │ │ │ ├── KawaseBlurShader.js
│ │ │ │ │ │ ├── ReactFixedNoiseEffect.ts
│ │ │ │ │ │ └── _BlurPass.ts
│ │ │ │ │ └── index.ts
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ │ └── index.ts
│ ├── hooks
│ │ ├── useHeroScreen.ts
│ │ └── usePrefersReducedMotion.ts
│ └── programs
│ │ ├── constants
│ │ └── index.ts
│ │ ├── contexts
│ │ └── index.tsx
│ │ ├── data
│ │ └── sessions.ts
│ │ └── types
│ │ └── index.ts
└── shared
│ ├── components
│ ├── Button
│ │ ├── Button.tsx
│ │ └── index.ts
│ ├── Column.tsx
│ ├── FadeIn.tsx
│ ├── Footer
│ │ ├── Footer.tsx
│ │ ├── components
│ │ │ ├── Logo.tsx
│ │ │ └── index.ts
│ │ ├── icons
│ │ │ └── index.tsx
│ │ └── index.ts
│ ├── Header
│ │ ├── Header.tsx
│ │ ├── components
│ │ │ ├── Logo.tsx
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── MainCTAButton
│ │ ├── MainCTAButton.tsx
│ │ ├── hooks
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── SectionTitle.tsx
│ └── index.ts
│ ├── constants
│ └── index.ts
│ └── icons
│ ├── LocationIcon.tsx
│ ├── TimeIcon.tsx
│ └── index.ts
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /**
2 | * This is intended to be a basic starting point for linting in your app.
3 | * It relies on recommended configs out of the box for simplicity, but you can
4 | * and should modify this configuration to best suit your team's needs.
5 | */
6 |
7 | /** @type {import('eslint').Linter.Config} */
8 | module.exports = {
9 | root: true,
10 | parserOptions: {
11 | ecmaVersion: 'latest',
12 | sourceType: 'module',
13 | ecmaFeatures: {
14 | jsx: true,
15 | },
16 | },
17 | env: {
18 | browser: true,
19 | commonjs: true,
20 | es6: true,
21 | },
22 |
23 | // Base config
24 | extends: ['eslint:recommended', 'eslint-config-prettier'],
25 | plugins: ['prettier'],
26 | rules: {
27 | 'prettier/prettier': 'error',
28 | },
29 |
30 | overrides: [
31 | // React
32 | {
33 | files: ['**/*.{js,jsx,ts,tsx}'],
34 | plugins: ['react', 'jsx-a11y'],
35 | extends: [
36 | 'plugin:react/recommended',
37 | 'plugin:react/jsx-runtime',
38 | 'plugin:react-hooks/recommended',
39 | 'plugin:jsx-a11y/recommended',
40 | ],
41 | settings: {
42 | react: {
43 | version: 'detect',
44 | },
45 | formComponents: ['Form'],
46 | linkComponents: [
47 | { name: 'Link', linkAttribute: 'to' },
48 | { name: 'NavLink', linkAttribute: 'to' },
49 | ],
50 | 'import/resolver': {
51 | typescript: {},
52 | },
53 | },
54 | rules: {
55 | 'react/display-name': 'off',
56 | 'react/no-unknown-property': 'off',
57 | },
58 | },
59 |
60 | // Typescript
61 | {
62 | files: ['**/*.{ts,tsx}'],
63 | plugins: ['@typescript-eslint', 'import'],
64 | parser: '@typescript-eslint/parser',
65 | settings: {
66 | 'import/internal-regex': '^~/',
67 | 'import/resolver': {
68 | node: {
69 | extensions: ['.ts', '.tsx'],
70 | },
71 | typescript: {
72 | alwaysTryTypes: true,
73 | },
74 | },
75 | },
76 | rules: {
77 | '@typescript-eslint/semi': ['error'],
78 | },
79 | extends: [
80 | 'plugin:@typescript-eslint/recommended',
81 | 'plugin:import/recommended',
82 | 'plugin:import/typescript',
83 | ],
84 | },
85 |
86 | // Node
87 | {
88 | files: ['.eslintrc.cjs'],
89 | env: {
90 | node: true,
91 | },
92 | },
93 | ],
94 | };
95 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | /out
5 |
6 |
7 | # Log files
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | pnpm-debug.log*
12 |
13 | # Editor directories and files
14 | .idea
15 | .vscode
16 | *.suo
17 | *.ntvs*
18 | *.njsproj
19 | *.sln
20 | *.sw?
21 |
22 | # ignore local settings
23 | .env.local
24 | .next
25 |
26 | ## Panda
27 | styled-system
28 | styled-system-studio
29 | .vercel
30 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "singleQuote": true,
4 | "trailingComma": "es5",
5 | "tabWidth": 2,
6 | "arrowParens": "avoid"
7 | }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 FEDG KR - Frontend Development Group Korea
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | output: 'export',
4 | };
5 |
6 | module.exports = nextConfig;
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "feconf2024",
3 | "version": "1.0.0",
4 | "private": true,
5 | "repository": "git@github.com:fedgkr/feconf2024.git",
6 | "contributors": [
7 | {
8 | "name": "Chaehyun Kim",
9 | "email": "elina001106@gmail.com",
10 | "url": "https://www.linkedin.com/in/chaehyunkim1106"
11 | },
12 | {
13 | "name": "Juyun Cheon",
14 | "email": "ewq3167@gmail.com",
15 | "url": "https://www.linkedin.com/in/juyun-cheon-713b37243"
16 | },
17 | {
18 | "name": "Daybrush",
19 | "email": "daybrush@gmail.com",
20 | "url": "https://github.com/daybrush"
21 | },
22 | {
23 | "name": "Jooyoung Moon",
24 | "email": "hckrmoon@gmail.com",
25 | "url": "https://github.com/codemilli"
26 | }
27 | ],
28 | "homepage": "https://2024.feconf.kr",
29 | "license": "MIT",
30 | "scripts": {
31 | "prepare": "panda codegen",
32 | "dev": "next dev",
33 | "build": "next build",
34 | "start": "next start",
35 | "lint": "next lint",
36 | "env": "touch out/.nojekyll && echo '2024.feconf.kr' >> out/CNAME",
37 | "push": "push-dir --dir=out --branch=gh-pages --cleanup --verbose",
38 | "deploy": "npm run build && npm run env && npm run push"
39 | },
40 | "dependencies": {
41 | "@radix-ui/react-dialog": "^1.1.1",
42 | "@react-three/drei": "^9.105.4",
43 | "@react-three/fiber": "^8.16.2",
44 | "@react-three/postprocessing": "^2.16.2",
45 | "framer-motion": "^11.3.2",
46 | "lodash-es": "^4.17.21",
47 | "next": "14.2.3",
48 | "react": "^18",
49 | "react-dom": "^18",
50 | "react-intersection-observer": "^9.13.0",
51 | "three": "^0.163.0",
52 | "use-intersection": "^0.2.1"
53 | },
54 | "devDependencies": {
55 | "@pandacss/dev": "^0.43.0",
56 | "@types/dat.gui": "^0.7.13",
57 | "@types/lodash-es": "^4.17.12",
58 | "@types/node": "^20",
59 | "@types/react": "^18",
60 | "@types/react-dom": "^18",
61 | "@types/three": "^0.163.0",
62 | "@typescript-eslint/eslint-plugin": "^6.7.4",
63 | "@typescript-eslint/parser": "^6.7.4",
64 | "dat.gui": "^0.7.9",
65 | "eslint": "^8.57.0",
66 | "eslint-config-next": "14.2.5",
67 | "eslint-config-prettier": "^9.1.0",
68 | "eslint-import-resolver-typescript": "^3.6.1",
69 | "eslint-plugin-import": "^2.28.1",
70 | "eslint-plugin-jsx-a11y": "^6.7.1",
71 | "eslint-plugin-prettier": "^5.1.3",
72 | "eslint-plugin-react": "^7.35.0",
73 | "eslint-plugin-react-hooks": "^4.6.0",
74 | "prettier": "^3.2.5",
75 | "push-dir": "^0.4.1",
76 | "typescript": "^5.1.6"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/panda.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "@pandacss/dev";
2 |
3 | export default defineConfig({
4 | // Whether to use css reset
5 | preflight: true,
6 |
7 | // Where to look for your css declarations
8 | include: ["./src/**/*.{js,jsx,ts,tsx}", "./pages/**/*.{js,jsx,ts,tsx}"],
9 |
10 | // Files to exclude
11 | exclude: [],
12 |
13 | // Useful for theme customization
14 | theme: {
15 | extend: {},
16 | },
17 |
18 | // The output directory for your css system
19 | outdir: "styled-system",
20 |
21 | // The JSX framework to use
22 | jsxFramework: "react",
23 | });
24 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | '@pandacss/dev/postcss': {},
4 | },
5 | }
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/public/favicon.ico
--------------------------------------------------------------------------------
/public/fe2024_og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/public/fe2024_og.png
--------------------------------------------------------------------------------
/src/app/components/ChildcareSection/ChildcareSection.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import { styled } from '@styled-system/jsx';
3 | import { Column, FadeIn, SectionTitle } from '~/shared/components';
4 |
5 | import jaranda from './assets/jaranda.png';
6 | import { motion, Variants } from 'framer-motion';
7 | import { useInView } from 'react-intersection-observer';
8 |
9 | const container: Variants = {
10 | visible: {
11 | transition: {
12 | staggerChildren: 0.15,
13 | delayChildren: 0.15,
14 | },
15 | },
16 | };
17 |
18 | const ChildcareSection: FC = () => {
19 | const { ref, inView } = useInView({
20 | triggerOnce: true,
21 | });
22 | return (
23 |
28 |
29 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | FEConf2024는 자녀 동반 참가자를 위한 현장 아이돌봄 케어 서비스를
40 | 제공합니다.
41 |
42 | 육아 케어 전문 기업 자란다와 함께 안전하고 보람찬 아이돌봄
43 | 서비스를 운영합니다.
44 |
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | const Section = styled(motion.section, {
53 | base: {
54 | position: 'relative',
55 | padding: {
56 | base: '60px 20px',
57 | xl: '150px 0',
58 | },
59 | },
60 | });
61 |
62 | const Info = styled(FadeIn, {
63 | base: {
64 | marginTop: {
65 | base: '50px',
66 | xl: '100px',
67 | },
68 | display: 'flex',
69 | width: '100%',
70 | maxWidth: '920px',
71 | padding: {
72 | base: '20px 25px 30px 25px',
73 | xl: '40px 25px 60px 25px',
74 | },
75 | flexDirection: 'column',
76 | justifyContent: 'center',
77 | alignItems: 'center',
78 | gap: {
79 | base: '20px',
80 | xl: '40px',
81 | },
82 | borderRadius: {
83 | base: '10px',
84 | xl: '20px',
85 | },
86 | background: 'rgba(78, 77, 96, 0.2)',
87 | boxShadow: '20px 20px 200px 0px rgba(1, 3, 8, 0.07)',
88 | },
89 | });
90 |
91 | const Image = styled('img', {
92 | base: {
93 | width: {
94 | base: '188px',
95 | xl: '357px',
96 | },
97 | height: {
98 | base: '59px',
99 | xl: '113px',
100 | },
101 | },
102 | });
103 |
104 | const Description = styled('p', {
105 | base: {
106 | color: 'rgba(255, 255, 255, 0.7)',
107 | textAlign: 'center',
108 | fontSize: {
109 | base: '14px',
110 | xl: '18px',
111 | },
112 | fontStyle: 'normal',
113 | fontWeight: '500',
114 | lineHeight: '160%' /* 28.8px */,
115 | },
116 | });
117 |
118 | export default ChildcareSection;
119 |
--------------------------------------------------------------------------------
/src/app/components/ChildcareSection/assets/jaranda.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/ChildcareSection/assets/jaranda.png
--------------------------------------------------------------------------------
/src/app/components/ChildcareSection/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './ChildcareSection';
2 |
--------------------------------------------------------------------------------
/src/app/components/CoCSection/CoCSection.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import { styled } from '@styled-system/jsx';
3 | import { Column, FadeIn, SectionTitle } from '~/shared/components';
4 | import { motion, Variants } from 'framer-motion';
5 | import { useInView } from 'react-intersection-observer';
6 |
7 | const container: Variants = {
8 | visible: {
9 | transition: {
10 | staggerChildren: 0.15,
11 | delayChildren: 0.15,
12 | },
13 | },
14 | };
15 |
16 | const CoCSection: FC = () => {
17 | const { ref, inView } = useInView({
18 | triggerOnce: true,
19 | });
20 | return (
21 |
26 |
27 |
31 |
32 |
33 |
34 | -
35 | 다양성
36 |
37 | FEConf는 개개인의 정체성과 개성 및 취향을 존중합니다. 하지만
38 | 성별, 성 정체성, 외모, 인종, 종교, 지역, 장애, 나이, 국가,
39 | 약자 등에 대한 혐오와 폭력은 어떤 방식이라도 허용하지
40 | 않습니다.
41 |
42 |
43 | -
44 | 사회적 책임
45 |
46 | FEConf참여자는 프론트엔드 분야의 성장에 대한 사회적 책임을
47 | 가집니다. 내가 알고 있는 지식은 아무리 작은 것이라도 다른
48 | 누군가에 도움을 줄 수 있습니다. 이를 다양한 방법으로
49 | 공유하세요.
50 |
51 |
52 |
53 |
54 |
55 |
56 | -
57 | 서로 돕고 협력하기
58 |
59 | 참여자의 다양한 배경이 협업과 커뮤니케이션을 방해하는 요소가
60 | 될 수 없습니다. 도움을 요청하기 전에 먼저 도움을 주고 자신의
61 | 생각을 자유롭게 표현할 수 있는 FEConf가 될 수 있도록 노력해
62 | 주세요.
63 |
64 |
65 | -
66 | 지식 재산권 및 개인 정보
67 |
68 | FEConf는 지식 재산권과 개인 정보 등의 권리를 존중합니다. 지식
69 | 재산권을 위배하거나 개인 정보를 침해하는 어떠한 콘텐츠도
70 | FEConf에서 사용할 수 없습니다.
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | );
79 | };
80 |
81 | const Section = styled(motion.section, {
82 | base: {
83 | position: 'relative',
84 | padding: {
85 | base: '60px 20px',
86 | xl: '150px 0',
87 | },
88 | },
89 | });
90 |
91 | const CoCList = styled('ul', {
92 | base: {
93 | display: 'flex',
94 | flexDirection: 'column',
95 | marginTop: {
96 | base: '50px',
97 | xl: '130px',
98 | },
99 | gap: {
100 | base: '20px',
101 | xl: '30px',
102 | },
103 | },
104 | });
105 |
106 | const CoCItem = styled('li', {
107 | base: {
108 | display: 'flex',
109 | },
110 | });
111 |
112 | const Wrap = styled(FadeIn, {
113 | base: {
114 | display: 'flex',
115 | justifyContent: 'center',
116 | flexDirection: {
117 | base: 'column',
118 | xl: 'row',
119 | },
120 | gap: {
121 | base: '20px',
122 | xl: '30px',
123 | },
124 | },
125 | });
126 |
127 | const Item = styled(FadeIn, {
128 | base: {
129 | display: 'flex',
130 | flexDirection: 'column',
131 | width: {
132 | base: '100%',
133 | xl: '540px',
134 | },
135 | padding: {
136 | base: '20px',
137 | xl: '40px',
138 | },
139 | alignItems: 'flex-start',
140 | flexShrink: 0,
141 | borderRadius: {
142 | base: '10px',
143 | xl: '20px',
144 | },
145 | background: 'rgba(78, 77, 96, 0.2)',
146 | },
147 | });
148 |
149 | const Title = styled('h4', {
150 | base: {
151 | color: '#fff',
152 | fontSize: '16px',
153 | fontStyle: 'normal',
154 | fontWeight: '700',
155 | lineHeight: '140%',
156 | },
157 | });
158 |
159 | const Description = styled('p', {
160 | base: {
161 | color: 'rgba(255, 255, 255, 0.8)',
162 | fontSize: '14px',
163 | fontStyle: 'normal',
164 | fontWeight: '400',
165 | lineHeight: '160%',
166 | marginTop: {
167 | base: '10px',
168 | xl: '20px',
169 | },
170 | },
171 | });
172 |
173 | export default CoCSection;
174 |
--------------------------------------------------------------------------------
/src/app/components/CoCSection/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './CoCSection';
2 |
--------------------------------------------------------------------------------
/src/app/components/ContactSection/ContactSection.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import { styled } from '@styled-system/jsx';
3 | import { Column, FadeIn, SectionTitle } from '~/shared/components';
4 | import { motion, Variants } from 'framer-motion';
5 | import { useInView } from 'react-intersection-observer';
6 | import { EMAIL, LINKED_IN_LINK } from '~/shared/constants';
7 |
8 | const container: Variants = {
9 | visible: {
10 | transition: {
11 | staggerChildren: 0.15,
12 | delayChildren: 0.15,
13 | },
14 | },
15 | };
16 |
17 | const ContactSection: FC = () => {
18 | const { ref, inView } = useInView({
19 | triggerOnce: true,
20 | });
21 | return (
22 |
27 |
28 |
29 |
30 | -
31 | Share
32 |
33 | 국내 최고의 프론트엔드 개발 인사이트를 얻을 수 있는 기회를
34 | 공유하여 함께 배우고, 함께 성장해보세요.
35 |
36 |
37 |
54 | LinkedIn
55 |
56 |
57 | -
58 | Contact
59 |
60 | 접근성 관련하여 행사 참석에 도움이 필요하실 경우, 메일로 편하게
61 | 연락주세요. 프론트엔드 개발자에 의한, 프론트엔드 개발자를 위한
62 | FEConf의 발전을 위해 도움을 주실 분도 언제든 환영합니다.
63 |
64 |
65 |
83 | 메일 보내기
84 |
85 |
86 |
87 |
88 |
89 | );
90 | };
91 |
92 | const Section = styled(motion.section, {
93 | base: {
94 | position: 'relative',
95 | padding: {
96 | base: '60px 20px',
97 | xl: '150px 0',
98 | },
99 | },
100 | });
101 |
102 | const Wrap = styled('div', {
103 | base: {
104 | marginTop: {
105 | base: '50px',
106 | xl: '130px',
107 | },
108 | display: 'flex',
109 | flexDirection: {
110 | base: 'column',
111 | xl: 'row',
112 | },
113 | gap: {
114 | base: '20px',
115 | xl: '50px',
116 | },
117 | },
118 | });
119 |
120 | const Item = styled(FadeIn, {
121 | base: {
122 | position: 'relative',
123 | display: 'flex',
124 | width: {
125 | base: '100%',
126 | xl: '540px',
127 | },
128 | height: {
129 | base: 'initial',
130 | xl: '336px',
131 | },
132 | padding: {
133 | base: '20px 20px 100px 20px',
134 | xl: '50px',
135 | },
136 | flexDirection: 'column',
137 | alignItems: 'flex-start',
138 | borderRadius: {
139 | base: '10px',
140 | xl: '20px',
141 | },
142 | background: 'rgba(78, 77, 96, 0.2)',
143 | },
144 | });
145 |
146 | const Title = styled('h4', {
147 | base: {
148 | color: '#fff',
149 | textAlign: 'center',
150 | fontSize: {
151 | base: '16px',
152 | xl: '30px',
153 | },
154 | fontStyle: 'normal',
155 | fontWeight: '500',
156 | lineHeight: '130%',
157 | letterSpacing: '1.2px',
158 | },
159 | });
160 |
161 | const Description = styled('div', {
162 | base: {
163 | marginTop: '20px',
164 | color: 'rgba(255, 255, 255, 0.8)',
165 | fontSize: '14px',
166 | fontStyle: 'normal',
167 | fontWeight: '500',
168 | lineHeight: '160%',
169 | },
170 | });
171 |
172 | const Link = styled('a', {
173 | base: {
174 | position: 'absolute',
175 | right: {
176 | base: '20px',
177 | xl: '52px',
178 | },
179 | bottom: {
180 | base: '30px',
181 | xl: '50px',
182 | },
183 | display: 'flex',
184 | padding: {
185 | base: '10px 14px',
186 | xl: '16px 22px',
187 | },
188 | justifyContent: 'center',
189 | alignItems: 'center',
190 | borderRadius: {
191 | base: '5px',
192 | xl: '10px',
193 | },
194 | gap: {
195 | base: '6px',
196 | xl: '12px',
197 | },
198 | color: '#010308',
199 | fontSize: {
200 | base: '14px',
201 | xl: '16px',
202 | },
203 | fontStyle: 'normal',
204 | fontWeight: '700',
205 | lineHeight: '130%',
206 | background: '#fff',
207 | cursor: 'pointer',
208 | backdropFilter: 'blur(3.1121041774749756px)',
209 | '& > svg': {
210 | width: {
211 | base: '20px',
212 | xl: 'initial',
213 | },
214 | height: {
215 | base: '20px',
216 | xl: 'initial',
217 | },
218 | },
219 | },
220 | });
221 |
222 | export default ContactSection;
223 |
--------------------------------------------------------------------------------
/src/app/components/ContactSection/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './ContactSection';
2 |
--------------------------------------------------------------------------------
/src/app/components/FooterSection/FooterSection.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import { styled } from '@styled-system/jsx';
3 | import { Column, FadeIn, MainCTAButton } from '~/shared/components';
4 | import {
5 | DataLocation,
6 | HeroLogo,
7 | } from '~/app/components/HeroSection/components';
8 | import { motion, Variants } from 'framer-motion';
9 | import { useInView } from 'react-intersection-observer';
10 |
11 | const container: Variants = {
12 | visible: {
13 | transition: {
14 | staggerChildren: 0.15,
15 | delayChildren: 0.15,
16 | },
17 | },
18 | };
19 |
20 | const FooterSection: FC = () => {
21 | const { ref, inView } = useInView({
22 | triggerOnce: true,
23 | });
24 | return (
25 |
30 |
31 |
32 | 8월 24일 토요일, FEConf와 함께해요
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | const Section = styled(motion.section, {
53 | base: {
54 | position: 'relative',
55 | marginTop: {
56 | base: '60px',
57 | xl: '150px',
58 | },
59 | marginBottom: '160px',
60 | padding: {
61 | base: '0 20px',
62 | },
63 | },
64 | });
65 |
66 | const Title = styled('h3', {
67 | base: {
68 | color: '#fff',
69 | textAlign: 'center',
70 | fontSize: {
71 | base: '18px',
72 | xl: '34px',
73 | },
74 | fontStyle: 'normal',
75 | fontWeight: '600',
76 | lineHeight: '140%',
77 | },
78 | });
79 |
80 | const LogoWrap = styled('div', {
81 | base: {
82 | marginTop: {
83 | base: '30px',
84 | xl: '50px',
85 | },
86 | '& > svg': {
87 | width: {
88 | base: '300px',
89 | xl: 'initial',
90 | },
91 | height: {
92 | base: '64px',
93 | xl: 'initial',
94 | },
95 | },
96 | },
97 | });
98 |
99 | const InfoWrap = styled('div', {
100 | base: {
101 | marginTop: {
102 | base: '40px',
103 | xl: '60px',
104 | },
105 | },
106 | });
107 |
108 | const ButtonWrap = styled(FadeIn, {
109 | base: {
110 | width: '100%',
111 | textAlign: 'center',
112 | marginTop: {
113 | base: '40px',
114 | xl: '90px',
115 | },
116 | },
117 | });
118 |
119 | export default FooterSection;
120 |
--------------------------------------------------------------------------------
/src/app/components/FooterSection/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './FooterSection';
2 |
--------------------------------------------------------------------------------
/src/app/components/HeroIntroWrap/HeroIntroWrap.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { FC, useEffect, useState } from 'react';
4 | import HeroSection from '../HeroSection';
5 | import IntroSection from '../IntroSection';
6 | import { useInView } from 'react-intersection-observer';
7 | import { useAurora } from '~/features/aurora/contexts';
8 |
9 | const HeroIntroWrap: FC = () => {
10 | const { ref: heroRef, inView: heroInView } = useInView({
11 | threshold: 0.3,
12 | triggerOnce: true,
13 | });
14 | const { ref: introRef, inView: introInView } = useInView({
15 | threshold: 0.3,
16 | triggerOnce: true,
17 | });
18 | const { show, hide } = useAurora();
19 | const [mounted, setMounted] = useState(false);
20 | useEffect(() => {
21 | setTimeout(() => setMounted(true), 200);
22 | }, []);
23 | useEffect(() => {
24 | const isInSection = heroInView || introInView;
25 | isInSection ? hide() : show();
26 | }, [heroInView, introInView, show, hide]);
27 | return (
28 | <>
29 |
30 |
31 | >
32 | );
33 | };
34 |
35 | export default HeroIntroWrap;
36 |
--------------------------------------------------------------------------------
/src/app/components/HeroIntroWrap/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './HeroIntroWrap';
2 |
--------------------------------------------------------------------------------
/src/app/components/HeroSection/HeroSection.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef } from 'react';
2 | import { styled } from '@styled-system/jsx';
3 | import { DataLocation, HeroLogo } from './components';
4 | import { Column, FadeIn, MainCTAButton } from '~/shared/components';
5 | import { motion, Variants } from 'framer-motion';
6 |
7 | interface Props {
8 | active?: boolean;
9 | }
10 |
11 | const container: Variants = {
12 | visible: {
13 | transition: {
14 | staggerChildren: 0.15,
15 | delayChildren: 0.2,
16 | },
17 | },
18 | };
19 |
20 | const HeroSection = forwardRef(({ active }, ref) => {
21 | return (
22 |
27 |
28 |
29 | 뜨거운 열정을 가진 당신이 올해의 주인공
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | });
44 |
45 | const Section = styled(motion.section, {
46 | base: {
47 | position: 'relative',
48 | height: '100vh',
49 | minHeight: '800px',
50 | },
51 | });
52 |
53 | const Container = styled(Column, {
54 | base: {
55 | position: 'absolute',
56 | top: '50%',
57 | width: '100%',
58 | padding: '0 20px',
59 | transform: 'translateY(-50%) translateY(-40px)',
60 | },
61 | });
62 |
63 | const Title = styled(FadeIn, {
64 | base: {
65 | fontSize: {
66 | base: '18px',
67 | xl: '24px',
68 | },
69 | fontWeight: '600',
70 | lineHeight: '1.3',
71 | color: '#ffffff',
72 | },
73 | });
74 |
75 | const LogoWrap = styled(FadeIn, {
76 | base: {
77 | marginTop: {
78 | base: '30px',
79 | xl: '50px',
80 | },
81 | '& svg': {
82 | width: {
83 | base: '300px',
84 | xl: 'initial',
85 | },
86 | height: {
87 | base: '64px',
88 | xl: 'initial',
89 | },
90 | },
91 | },
92 | });
93 |
94 | const Info = styled(FadeIn, {
95 | base: {
96 | marginTop: {
97 | base: '40px',
98 | xl: '78px',
99 | },
100 | },
101 | });
102 |
103 | const ButtonWrap = styled(FadeIn, {
104 | base: {
105 | width: '100%',
106 | textAlign: 'center',
107 | marginTop: {
108 | base: '50px',
109 | xl: '90px',
110 | },
111 | },
112 | });
113 |
114 | export default HeroSection;
115 |
--------------------------------------------------------------------------------
/src/app/components/HeroSection/components/DataLocation.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import { styled } from '@styled-system/jsx';
3 | import { LocationIcon, TimeIcon } from '~/shared/icons';
4 |
5 | const DataLocation: FC = () => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 | 2024.8.24
13 |
14 |
15 |
16 |
17 |
18 | 세종대학교 광개토회관
19 |
20 |
21 | );
22 | };
23 |
24 | const Container = styled('div', {
25 | base: {
26 | display: 'flex',
27 | gap: {
28 | base: '12px',
29 | xl: '30px',
30 | },
31 | },
32 | });
33 |
34 | const Wrap = styled('div', {
35 | base: {
36 | display: 'flex',
37 | alignItems: 'center',
38 | gap: {
39 | base: '8px',
40 | xl: '20px',
41 | },
42 | },
43 | });
44 |
45 | const IconWrap = styled('div', {
46 | base: {
47 | display: 'flex',
48 | alignItems: 'center',
49 | justifyContent: 'center',
50 | width: {
51 | base: '26px',
52 | xl: '34px',
53 | },
54 | height: {
55 | base: '26px',
56 | xl: '34px',
57 | },
58 | borderRadius: '10px',
59 | background: 'rgba(255, 255, 255, 0.3)',
60 | '& svg': {
61 | width: {
62 | base: '15px',
63 | xl: 'initial',
64 | },
65 | height: {
66 | base: '15px',
67 | xl: 'initial',
68 | },
69 | },
70 | },
71 | });
72 |
73 | const Text = styled('span', {
74 | base: {
75 | color: '#fff',
76 | textAlign: 'center',
77 | fontVariantNumeric: 'lining-nums tabular-nums',
78 | fontSize: {
79 | base: '14px',
80 | xl: '20px',
81 | },
82 | fontStyle: 'normal',
83 | fontWeight: '600',
84 | lineHeight: '140%',
85 | },
86 | });
87 |
88 | export default DataLocation;
89 |
--------------------------------------------------------------------------------
/src/app/components/HeroSection/components/HeroLogo.tsx:
--------------------------------------------------------------------------------
1 | import { FC, SVGProps } from 'react';
2 |
3 | interface Props extends SVGProps {}
4 |
5 | const HeroLogo: FC = ({ ...props }) => {
6 | return (
7 |
53 | );
54 | };
55 |
56 | export default HeroLogo;
57 |
--------------------------------------------------------------------------------
/src/app/components/HeroSection/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as DataLocation } from './DataLocation';
2 | export { default as HeroLogo } from './HeroLogo';
3 |
--------------------------------------------------------------------------------
/src/app/components/HeroSection/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './HeroSection';
2 |
--------------------------------------------------------------------------------
/src/app/components/IntroSection/IntroSection.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef } from 'react';
2 | import { styled } from '@styled-system/jsx';
3 | import { Column, FadeIn } from '~/shared/components';
4 | import { motion, Variants } from 'framer-motion';
5 | import { SphereContainer } from '~/features/effects/components/SphereContainer';
6 |
7 | interface Props {
8 | active?: boolean;
9 | }
10 |
11 | const container: Variants = {
12 | visible: {
13 | transition: {
14 | staggerChildren: 0.15,
15 | delayChildren: 0.3,
16 | },
17 | },
18 | };
19 |
20 | const IntroSection = forwardRef(({ active }, ref) => {
21 | return (
22 |
27 |
28 |
29 |
30 |
31 | 프론트엔드 엔지니어들에 의한,
32 |
33 |
34 |
35 |
36 | 프론트엔드 엔지니어들을 위한
37 |
38 |
39 |
40 |
41 | 국내 최고 컨퍼런스 FEConf
42 |
43 |
44 |
45 |
46 | 더 나은 프론트엔드 엔지니어링을 위해 노력하는 이들이 함께 모여
47 | 현장에서 겪는 다양한 문제를 함께 공유하고 성장합니다.
48 |
49 |
50 |
51 |
52 | );
53 | });
54 |
55 | const Section = styled(motion.section, {
56 | base: {
57 | position: 'relative',
58 | padding: {
59 | base: '200px 0 300px 0',
60 | xl: '180px 0 300px 0',
61 | },
62 | },
63 | });
64 |
65 | const Title = styled('h1', {
66 | base: {
67 | fontSize: {
68 | base: '26px',
69 | xl: '54px',
70 | },
71 | fontWeight: '700',
72 | lineHeight: '2',
73 | color: 'rgba(1, 3, 8, 0.5)',
74 | },
75 | });
76 |
77 | const Description = styled('p', {
78 | base: {
79 | width: {
80 | base: '313px',
81 | xl: '492px',
82 | },
83 | marginTop: {
84 | base: '60px',
85 | xl: '50px',
86 | },
87 | fontSize: {
88 | base: '16px',
89 | xl: '20px',
90 | },
91 | fontWeight: '600',
92 | lineHeight: '1.6',
93 | color: 'rgba(1, 3, 8, 0.8)',
94 | textAlign: 'center',
95 | },
96 | });
97 |
98 | const Highlight = styled('span', {
99 | base: {
100 | position: 'relative',
101 | display: 'inline-block',
102 | lineHeight: '1',
103 | color: 'black',
104 | },
105 | });
106 |
107 | export default IntroSection;
108 |
--------------------------------------------------------------------------------
/src/app/components/IntroSection/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './IntroSection';
2 |
--------------------------------------------------------------------------------
/src/app/components/OpenSourceSection/OpenSourceSection.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import { styled } from '@styled-system/jsx';
3 | import { Column, FadeIn, SectionTitle } from '~/shared/components';
4 |
5 | import pretendardLogo from './assets/pretendard.png';
6 | import zodLogo from './assets/zod.png';
7 | import kyLogo from './assets/ky.png';
8 | import queryLogo from './assets/query.png';
9 | import { motion, Variants } from 'framer-motion';
10 | import { useInView } from 'react-intersection-observer';
11 |
12 | const container: Variants = {
13 | visible: {
14 | transition: {
15 | staggerChildren: 0.15,
16 | delayChildren: 0.15,
17 | },
18 | },
19 | };
20 |
21 | const OpenSourceSection: FC = () => {
22 | const { ref, inView } = useInView({
23 | triggerOnce: true,
24 | });
25 | return (
26 |
31 |
32 |
36 |
37 |
38 |
39 | -
40 |
45 |
46 | -
47 |
48 |
49 | -
50 |
51 |
52 | -
53 |
54 |
55 |
56 |
57 |
58 |
59 | 프론트엔드 오픈소스 프로젝트에 대한 감사의 마음을 담아 FEConf에서
60 | 후원을 진행합니다.
61 | 행사 현장에서 오픈소스 이벤트에 참여하고, 후원할 프로젝트에 직접
62 | 투표하세요!
63 | 여러분의 투표 결과에 따라 오픈소스 프로젝트에 대한 후원 금액이
64 | 결정됩니다.
65 |
66 |
67 |
68 |
69 |
70 | );
71 | };
72 |
73 | const Section = styled(motion.section, {
74 | base: {
75 | position: 'relative',
76 | padding: {
77 | base: '60px 20px',
78 | xl: '150px 0',
79 | },
80 | },
81 | });
82 |
83 | const Wrap = styled(FadeIn, {
84 | base: {
85 | display: 'flex',
86 | width: '100%',
87 | maxWidth: '920px',
88 | marginTop: {
89 | base: '50px',
90 | xl: '130px',
91 | },
92 | padding: {
93 | base: '20px 16px 30px 16px',
94 | xl: '40px 25px 60px 25px',
95 | },
96 | flexDirection: 'column',
97 | justifyContent: 'center',
98 | alignItems: 'center',
99 | gap: {
100 | base: '20px',
101 | xl: '40px',
102 | },
103 | borderRadius: {
104 | base: '10px',
105 | xl: '20px',
106 | },
107 | background: 'rgba(78, 77, 96, 0.2)',
108 | boxShadow: '20px 20px 200px 0px rgba(1, 3, 8, 0.07)',
109 | },
110 | });
111 |
112 | const List = styled('ul', {
113 | base: {
114 | margin: {
115 | base: '0 auto',
116 | },
117 | display: 'flex',
118 | width: {
119 | base: '100%',
120 | },
121 | gap: {
122 | base: '10px',
123 | xl: '20px',
124 | },
125 | justifyContent: 'center',
126 | alignItems: 'center',
127 | flexWrap: 'wrap',
128 | },
129 | });
130 |
131 | const Item = styled('li', {
132 | base: {
133 | display: 'flex',
134 | alignItems: 'center',
135 | height: {
136 | base: '50px',
137 | xl: '70px',
138 | },
139 | boxSizing: 'content-box',
140 | marginTop: '16px',
141 | },
142 | });
143 |
144 | const Image = styled('img', {
145 | base: {
146 | height: {
147 | base: '50px',
148 | xl: '70px',
149 | },
150 | },
151 | });
152 |
153 | const Description = styled('p', {
154 | base: {
155 | color: 'rgba(255, 255, 255, 0.7)',
156 | textAlign: 'center',
157 | fontSize: {
158 | base: '14px',
159 | xl: '18px',
160 | },
161 | fontStyle: 'normal',
162 | fontWeight: '500',
163 | lineHeight: '160%',
164 | },
165 | });
166 |
167 | const Br = styled('br', {
168 | base: {
169 | display: {
170 | base: 'none',
171 | xl: 'initial',
172 | },
173 | },
174 | });
175 |
176 | export default OpenSourceSection;
177 |
--------------------------------------------------------------------------------
/src/app/components/OpenSourceSection/assets/babel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/OpenSourceSection/assets/babel.png
--------------------------------------------------------------------------------
/src/app/components/OpenSourceSection/assets/core-js.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/OpenSourceSection/assets/core-js.png
--------------------------------------------------------------------------------
/src/app/components/OpenSourceSection/assets/ky.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/OpenSourceSection/assets/ky.png
--------------------------------------------------------------------------------
/src/app/components/OpenSourceSection/assets/pretendard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/OpenSourceSection/assets/pretendard.png
--------------------------------------------------------------------------------
/src/app/components/OpenSourceSection/assets/query.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/OpenSourceSection/assets/query.png
--------------------------------------------------------------------------------
/src/app/components/OpenSourceSection/assets/react-hook-form.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/OpenSourceSection/assets/react-hook-form.png
--------------------------------------------------------------------------------
/src/app/components/OpenSourceSection/assets/ts-pattern.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/OpenSourceSection/assets/ts-pattern.png
--------------------------------------------------------------------------------
/src/app/components/OpenSourceSection/assets/zod.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/OpenSourceSection/assets/zod.png
--------------------------------------------------------------------------------
/src/app/components/OpenSourceSection/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './OpenSourceSection';
2 |
--------------------------------------------------------------------------------
/src/app/components/ProgramSection/ProgramSection.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import { Column, FadeIn, SectionTitle } from '~/shared/components';
3 | import { styled } from '@styled-system/jsx';
4 | import { ProgramTab, ProgramHeader, SessionList } from './components';
5 | import { ProgramContextProvider } from '~/features/programs/contexts';
6 | import { motion, Variants } from 'framer-motion';
7 |
8 | import timetable from './assets/timetable.jpg';
9 | import { useInView } from 'react-intersection-observer';
10 |
11 | const container: Variants = {
12 | visible: {
13 | transition: {
14 | staggerChildren: 0.15,
15 | delayChildren: 0.15,
16 | },
17 | },
18 | };
19 |
20 | const ProgramSection: FC = () => {
21 | const { ref, inView } = useInView({
22 | triggerOnce: true,
23 | });
24 | return (
25 |
26 |
31 |
32 |
36 |
37 |
38 |
39 |
40 |
41 | 타임 테이블 이미지 다운로드
42 |
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | const Section = styled(motion.section, {
51 | base: {
52 | position: 'relative',
53 | padding: {
54 | base: '60px 20px',
55 | xl: '150px 0',
56 | },
57 | },
58 | });
59 |
60 | const DownloadTimetable = styled('a', {
61 | base: {
62 | marginTop: {
63 | base: '40px',
64 | xl: '150px',
65 | },
66 | marginLeft: 'auto',
67 | marginRight: 'auto',
68 | display: 'flex',
69 | maxWidth: {
70 | base: 'initial',
71 | xl: '350px',
72 | },
73 | width: '100%',
74 | height: {
75 | base: '54px',
76 | xl: '64px',
77 | },
78 | padding: '20px 60px',
79 | justifyContent: 'center',
80 | alignItems: 'center',
81 | gap: '8px',
82 | borderRadius: '10px',
83 | color: '#010308',
84 | fontSize: {
85 | base: '16px',
86 | xl: '20px',
87 | },
88 | fontStyle: 'normal',
89 | fontWeight: '700',
90 | lineHeight: '130%',
91 | cursor: 'pointer',
92 | background: '#fff',
93 | backdropFilter: 'blur(3.1121041774749756px)',
94 | },
95 | });
96 |
97 | const ButtonWrap = styled(FadeIn, {
98 | base: {
99 | width: '100%',
100 | },
101 | });
102 |
103 | export default ProgramSection;
104 |
--------------------------------------------------------------------------------
/src/app/components/ProgramSection/assets/timetable.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/ProgramSection/assets/timetable.jpg
--------------------------------------------------------------------------------
/src/app/components/ProgramSection/components/ProgramHeader.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { FC } from 'react';
4 | import { SessionType } from '~/features/programs/types';
5 | import { styled } from '@styled-system/jsx';
6 | import { useProgram } from '~/features/programs/contexts';
7 | import { ArrowIcon } from './components';
8 | import { get, indexOf, lt, size } from 'lodash-es';
9 | import { FadeIn } from '~/shared/components';
10 |
11 | type HeaderInfo = {
12 | title: string;
13 | description: string;
14 | icon?: string;
15 | };
16 |
17 | const programList = [SessionType.A, SessionType.B, SessionType.Lightning];
18 |
19 | const headerLookup: Record = {
20 | [SessionType.A]: {
21 | title: 'Speaker',
22 | description:
23 | '2024년, 국내 최정상 프론트 엔지니어들의 가장 뜨거웠던 현장 경험을 공유합니다',
24 | icon: 'A',
25 | },
26 | [SessionType.B]: {
27 | title: 'Speaker',
28 | description:
29 | '2024년, 국내 최정상 프론트 엔지니어들의 가장 뜨거웠던 현장 경험을 공유합니다',
30 | icon: 'B',
31 | },
32 | [SessionType.Lightning]: {
33 | title: 'Lightning Talk',
34 | description:
35 | '짧지만 강렬하게 프론트엔드 개발에 대한 진솔한 이야기를 나누며 함께 공감하고 토론하세요',
36 | },
37 | };
38 |
39 | const ProgramHeader: FC = () => {
40 | const { currentTab, onChangeTab } = useProgram();
41 | const { title, description, icon } = headerLookup[currentTab];
42 | const index = indexOf(programList, currentTab);
43 | const hasPrev = index !== 0;
44 | const hasNext = lt(index, size(programList) - 1);
45 | const handleClickPrev = () => {
46 | const target = get(programList, index - 1);
47 | onChangeTab(target);
48 | };
49 | const handleClickNext = () => {
50 | const target = get(programList, index + 1);
51 | onChangeTab(target);
52 | };
53 | return (
54 |
55 |
56 |
57 | {title}
58 | {icon && {icon}}
59 |
60 |
61 |
62 | {description}
63 |
64 | {hasPrev && (
65 |
69 |
70 |
71 | )}
72 | {hasNext && (
73 |
74 |
75 |
76 | )}
77 |
78 | );
79 | };
80 |
81 | const Container = styled(FadeIn, {
82 | base: {
83 | position: 'relative',
84 | width: '100%',
85 | marginTop: {
86 | base: '20px',
87 | xl: '50px',
88 | },
89 | padding: {
90 | base: '20px 22px',
91 | xl: '45px 0',
92 | },
93 | borderRadius: {
94 | base: '10px',
95 | xl: '20px',
96 | },
97 | border: '1px solid rgba(255, 255, 255, 0.1)',
98 | background: 'rgba(33, 35, 50, 0.2)',
99 | },
100 | });
101 |
102 | const Title = styled('h2', {
103 | base: {
104 | display: 'flex',
105 | justifyContent: 'center',
106 | alignItems: 'center',
107 | fontSize: {
108 | base: '20px',
109 | xl: '40px',
110 | },
111 | fontWeight: '500',
112 | color: '#fff',
113 | lineHeight: '1.3',
114 | textAlign: 'center',
115 | },
116 | });
117 |
118 | const Icon = styled('div', {
119 | base: {
120 | width: {
121 | base: '24px',
122 | xl: '35px',
123 | },
124 | height: {
125 | base: '24px',
126 | xl: '35px',
127 | },
128 | marginLeft: {
129 | base: '6px',
130 | xl: '20px',
131 | },
132 | borderRadius: {
133 | base: '5px',
134 | xl: '10px',
135 | },
136 | display: 'inline-flex',
137 | alignItems: 'center',
138 | justifyContent: 'center',
139 | color: 'white',
140 | fontSize: {
141 | base: '12px',
142 | xl: '18px',
143 | },
144 | fontWeight: '400',
145 | backgroundColor: 'rgba(78, 77, 96, 0.3)',
146 | },
147 | });
148 |
149 | const Description = styled('p', {
150 | base: {
151 | marginTop: {
152 | base: '20px',
153 | xl: '13px',
154 | },
155 | color: 'rgba(255, 255, 255, 0.7)',
156 | textAlign: 'center',
157 | fontSize: {
158 | base: '14px',
159 | xl: '16px',
160 | },
161 | fontStyle: 'normal',
162 | fontWeight: '500',
163 | lineHeight: '160%',
164 | },
165 | });
166 |
167 | const NavWrap = styled('button', {
168 | base: {
169 | position: 'absolute',
170 | top: '0',
171 | bottom: '0',
172 | width: '35px',
173 | height: '35px',
174 | display: {
175 | base: 'none',
176 | xl: 'flex',
177 | },
178 | justifyContent: 'center',
179 | alignItems: 'center',
180 | margin: 'auto 0',
181 | border: '1px solid rgba(255, 255, 255, 0.1)',
182 | borderRadius: '10px',
183 | cursor: 'pointer',
184 | },
185 | });
186 |
187 | export default ProgramHeader;
188 |
--------------------------------------------------------------------------------
/src/app/components/ProgramSection/components/ProgramTab.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { FC } from 'react';
4 | import { SessionType } from '~/features/programs/types';
5 | import { styled } from '@styled-system/jsx';
6 | import { eq, map } from 'lodash-es';
7 | import { useProgram } from '~/features/programs/contexts';
8 | import { FadeIn } from '~/shared/components';
9 |
10 | interface Props {}
11 |
12 | const tabList = [SessionType.A, SessionType.B, SessionType.Lightning];
13 |
14 | const labelLookup: Record = {
15 | [SessionType.A]: 'Speaker A',
16 | [SessionType.B]: 'Speaker B',
17 | [SessionType.Lightning]: 'Lightning Talk',
18 | };
19 |
20 | const ProgramTab: FC = () => {
21 | const { currentTab, onChangeTab } = useProgram();
22 | return (
23 |
24 |
25 | {map(tabList, tab => (
26 | onChangeTab(tab)}
30 | >
31 |
32 |
33 | ))}
34 |
35 |
36 | );
37 | };
38 |
39 | const List = styled('ul', {
40 | base: {
41 | display: 'flex',
42 | marginTop: {
43 | base: '50px',
44 | xl: '100px',
45 | },
46 | padding: {
47 | base: '8px',
48 | xl: '10px',
49 | },
50 | borderRadius: {
51 | base: '10px',
52 | xl: '12px',
53 | },
54 | gap: {
55 | base: '10px',
56 | xl: '10px',
57 | },
58 | backgroundColor: 'rgba(78, 77, 96, 0.2)',
59 | },
60 | });
61 |
62 | const Tab = styled('li', {
63 | base: {
64 | display: 'inline-block',
65 | padding: {
66 | base: '4px 8px',
67 | xl: '6px 14px',
68 | },
69 | cursor: 'pointer',
70 | color: '#fff',
71 | fontSize: {
72 | base: '12px',
73 | xl: '18px',
74 | },
75 | fontWeight: 500,
76 | lineHeight: '140%',
77 | borderRadius: {
78 | base: '5px',
79 | xl: '8px',
80 | },
81 | textAlign: 'center',
82 | transition: 'background-color 200ms ease-out',
83 | },
84 | variants: {
85 | active: {
86 | true: {
87 | backgroundColor: 'rgba(78, 77, 96, 0.50)',
88 | },
89 | false: {
90 | backgroundColor: 'transparent',
91 | },
92 | },
93 | },
94 | });
95 |
96 | const Button = styled('button', {
97 | base: {
98 | cursor: 'pointer',
99 | },
100 | });
101 |
102 | export default ProgramTab;
103 |
--------------------------------------------------------------------------------
/src/app/components/ProgramSection/components/SessionList.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { FC, useState } from 'react';
4 | import { styled } from '@styled-system/jsx';
5 | import { map, size } from 'lodash-es';
6 | import { FadeIn } from '~/shared/components';
7 | import type { Session } from '~/features/programs/types';
8 | import { SessionType } from '~/features/programs/types';
9 | import {
10 | lightningSessionTimeLabelLookup,
11 | sessionTimeLabelLookup,
12 | } from '~/features/programs/constants';
13 |
14 | import { ClockIcon, SessionModal } from './components';
15 | import { useProgram } from '~/features/programs/contexts';
16 | import {
17 | aSessionList,
18 | bSessionList,
19 | lightningSessionList,
20 | } from '~/features/programs/data/sessions';
21 | import { match } from 'ts-pattern';
22 |
23 | const sessionsLookup: Record = {
24 | [SessionType.A]: aSessionList,
25 | [SessionType.B]: bSessionList,
26 | [SessionType.Lightning]: lightningSessionList,
27 | };
28 |
29 | const SessionList: FC = () => {
30 | const { currentTab } = useProgram();
31 | const [currentSession, setCurrentSession] = useState(null);
32 | const [open, setOpen] = useState(false);
33 | const sessions = sessionsLookup[currentTab];
34 | const handleClickSession = (session: Session) => {
35 | setCurrentSession(session);
36 | setOpen(true);
37 | };
38 | const handleChangeOpen = (open: boolean) => {
39 | if (!open) setOpen(open);
40 | };
41 | const timeLabelLookup = match(currentTab)
42 | .with(SessionType.Lightning, () => lightningSessionTimeLabelLookup)
43 | .otherwise(() => sessionTimeLabelLookup);
44 | const length = sessions.length;
45 | return (
46 |
47 |
48 |
49 | {map(sessions, (session, index) => {
50 | const { title, speakers, order } = session;
51 | return (
52 | handleClickSession(session)}
56 | >
57 |
66 |
72 |
73 |
74 | {title}
75 |
76 | {map(speakers, (speaker, index) => (
77 |
78 | {speaker?.name}
79 | {index < size(speakers) - 1 ? ', ' : ''}
80 |
81 | ))}
82 |
83 |
84 |
85 |
86 | );
87 | })}
88 |
93 |
94 | );
95 | };
96 |
97 | const Container = styled('div', {
98 | base: {
99 | position: 'relative',
100 | marginTop: {
101 | base: '40px',
102 | xl: '100px',
103 | },
104 | paddingTop: {
105 | base: '70px',
106 | xl: '0px',
107 | },
108 | width: '100%',
109 | },
110 | });
111 |
112 | const Session = styled(FadeIn, {
113 | base: {
114 | position: 'relative',
115 | width: '100%',
116 | display: 'flex',
117 | alignItems: 'flex-start',
118 | flexDirection: {
119 | base: 'column',
120 | xl: 'row',
121 | },
122 | cursor: 'pointer',
123 | '&:not(:first-of-type)': {
124 | marginTop: {
125 | base: '30px',
126 | xl: '60px',
127 | },
128 | },
129 | },
130 | });
131 |
132 | const Line = styled(FadeIn, {
133 | base: {
134 | '--session-margin-top': {
135 | base: '30px',
136 | xl: '60px',
137 | },
138 | '--session-time-top': {
139 | base: '0px',
140 | xl: '58px',
141 | },
142 | '--session-time-height': '24px',
143 | '--session-time-center': 'calc(var(--session-time-height) / 2)',
144 | position: 'absolute',
145 | top: {
146 | base: 'calc(-1 * var(--session-margin-top))',
147 | xl: 'calc(var(--session-time-top) + var(--session-time-center))',
148 | },
149 | height: {
150 | // 24px 원이 포함된 영역 크기
151 | // 14px 그 사이 여백
152 | base: 'calc(var(--session-margin-top) + var(--session-time-height) + 14px)',
153 | xl: 'calc(100% + var(--session-margin-top))',
154 | },
155 | left: {
156 | base: '15px',
157 | xl: '135px',
158 | },
159 | width: '1px',
160 | background: 'rgba(255, 255, 255, 10%)',
161 | '&.session-time-line': {
162 | top: '35px',
163 | height:
164 | 'calc(var(--session-margin-top) + var(--session-time-top) + var(--session-time-center) - 35px)',
165 | display: {
166 | base: 'none',
167 | xl: 'block',
168 | },
169 | },
170 | '&.session-first-line': {
171 | // padding-top 35px
172 | top: {
173 | base: 'calc(-1 * var(--session-margin-top) - 35px)',
174 | xl: 'calc(var(--session-time-top) + var(--session-time-center))',
175 | },
176 | height: {
177 | // 24px 원이 포함된 영역 크기
178 | // 14px 그 사이 여백
179 | base: 'calc(var(--session-margin-top) + var(--session-time-height) + 14px + 35px)',
180 | xl: 'calc(100% + var(--session-margin-top))',
181 | },
182 | },
183 | '&.session-last-line': {
184 | display: {
185 | base: 'block',
186 | xl: 'none',
187 | },
188 | },
189 | },
190 | });
191 | const TimeIcon = styled(ClockIcon, {
192 | base: {
193 | position: 'absolute',
194 | top: '0px',
195 | left: {
196 | base: '-2px',
197 | xl: '118px',
198 | },
199 | opacity: '0',
200 | transform: 'translateY(30px)',
201 | padding: '9px 8px 8px 9px',
202 | boxSizing: 'content-box !important',
203 | borderRadius: '10px',
204 | backgroundColor: 'rgba(78, 77, 96, 0.2)',
205 | },
206 | });
207 |
208 | const Time = styled('div', {
209 | base: {
210 | position: 'relative',
211 | display: 'flex',
212 | flexGrow: 0,
213 | flexDirection: {
214 | base: 'row-reverse',
215 | xl: 'row',
216 | },
217 | color: 'rgba(255, 255, 255, 0.8)',
218 | textAlign: 'right',
219 | fontSize: '16px',
220 | fontStyle: 'normal',
221 | fontWeight: '500',
222 | marginTop: {
223 | base: 'initial',
224 | xl: '58px',
225 | },
226 | // 100px (시간) + 86px (충분한 오른쪽 여백)
227 | width: {
228 | base: 'initial',
229 | xl: '186px',
230 | },
231 | // marginRight: {
232 | // base: 'initial',
233 | // xl: '84px',
234 | // },
235 | paddingLeft: {
236 | base: '31px',
237 | xl: 'initial',
238 | },
239 | },
240 | });
241 |
242 | const LiveIcon = styled('div', {
243 | base: {
244 | position: 'absolute',
245 | display: 'flex',
246 | alignItems: 'center',
247 | justifyContent: 'center',
248 | top: '50%',
249 | right: {
250 | base: 'initial',
251 | // 여백을 줬기 때문에 자체적인 right 값 부여
252 | xl: '58px',
253 | },
254 | left: {
255 | base: '8px',
256 | xl: 'initial',
257 | },
258 | width: '15px',
259 | height: '15px',
260 | fontSize: 0,
261 | borderRadius: '50%',
262 | background: 'rgba(255, 255, 255, 0.35)',
263 | transform: {
264 | base: 'translate(0, -50%)',
265 | xl: 'translate(100%, -50%)',
266 | },
267 | },
268 | });
269 |
270 | const IconChild = styled('div', {
271 | base: {
272 | width: '7px',
273 | height: '7px',
274 | borderRadius: '50%',
275 | background: 'rgba(255, 255, 255, 0.8)',
276 | },
277 | });
278 |
279 | const SessionInfo = styled('div', {
280 | base: {
281 | width: '100%',
282 | flex: 1,
283 | borderRadius: {
284 | base: '10px',
285 | xl: '20px',
286 | },
287 | marginTop: {
288 | base: '14px',
289 | xl: 'initial',
290 | },
291 | background: 'rgba(78, 77, 96, 0.2)',
292 | },
293 | });
294 |
295 | const Info = styled('div', {
296 | base: {
297 | padding: {
298 | base: '16px',
299 | xl: '30px 180px 45px 40px',
300 | },
301 | },
302 | });
303 |
304 | const Title = styled('div', {
305 | base: {
306 | color: '#fff',
307 | fontSize: {
308 | base: '16px',
309 | xl: '28px',
310 | },
311 | fontStyle: 'normal',
312 | fontWeight: '600',
313 | lineHeight: '140%',
314 | },
315 | });
316 |
317 | const SpeakerInfo = styled('div', {
318 | base: {
319 | marginTop: {
320 | base: '10px',
321 | xl: '24px',
322 | },
323 | color: 'rgba(255, 255, 255, 0.5)',
324 | fontSize: {
325 | base: '12px',
326 | xl: '18px',
327 | },
328 | fontStyle: 'normal',
329 | fontWeight: '500',
330 | lineHeight: '100%',
331 | },
332 | });
333 |
334 | export default SessionList;
335 |
--------------------------------------------------------------------------------
/src/app/components/ProgramSection/components/components/ArrowIcon.tsx:
--------------------------------------------------------------------------------
1 | import { FC, SVGProps } from 'react';
2 |
3 | const ArrowIcon: FC> = props => (
4 |
20 | );
21 |
22 | export default ArrowIcon;
23 |
--------------------------------------------------------------------------------
/src/app/components/ProgramSection/components/components/ClockIcon.tsx:
--------------------------------------------------------------------------------
1 | import { FC, SVGProps } from 'react';
2 | import { motion } from 'framer-motion';
3 |
4 | interface Props extends SVGProps {}
5 |
6 | const variants = {
7 | visible: {
8 | opacity: 1,
9 | y: 0,
10 | transition: { duration: 0.4 },
11 | },
12 | hidden: {
13 | opacity: 0,
14 | y: `30px`,
15 | transition: { duration: 0.25 },
16 | },
17 | };
18 |
19 | const ClockIcon: FC = props => {
20 | return (
21 |
30 |
37 |
38 | );
39 | };
40 |
41 | export default ClockIcon;
42 |
--------------------------------------------------------------------------------
/src/app/components/ProgramSection/components/components/CloseIcon.tsx:
--------------------------------------------------------------------------------
1 | import { FC, SVGProps } from 'react';
2 |
3 | interface Props extends SVGProps {}
4 |
5 | const CloseIcon: FC = props => {
6 | return (
7 |
23 | );
24 | };
25 |
26 | export default CloseIcon;
27 |
--------------------------------------------------------------------------------
/src/app/components/ProgramSection/components/components/SessionModal.tsx:
--------------------------------------------------------------------------------
1 | import { FC, Fragment } from 'react';
2 |
3 | import * as Dialog from '@radix-ui/react-dialog';
4 | import { styled } from '@styled-system/jsx';
5 | import { Session, SessionType } from '~/features/programs/types';
6 | import {
7 | lightningSessionTimeLabelLookup,
8 | sessionTimeLabelLookup,
9 | } from '~/features/programs/constants';
10 |
11 | import CloseIcon from './CloseIcon';
12 | import { map, size } from 'lodash-es';
13 |
14 | interface Props {
15 | session: Session | null;
16 | open: boolean;
17 | onChangeOpen: (open: boolean) => void;
18 | }
19 |
20 | const labelLookup: Record = {
21 | [SessionType.A]: 'Speaker A',
22 | [SessionType.B]: 'Speaker B',
23 | [SessionType.Lightning]: 'Lightning Talk',
24 | };
25 |
26 | const SessionModal: FC = ({ session, open, onChangeOpen }) => {
27 | const timeLabelLookup =
28 | session?.type === SessionType.Lightning
29 | ? lightningSessionTimeLabelLookup
30 | : sessionTimeLabelLookup;
31 | return (
32 |
33 |
34 |
35 |
36 |
37 | {session && (
38 |
42 | )}
43 |
44 |
45 |
46 |
47 | {session && (
48 | <>
49 | {session.title}
50 |
53 |
54 | {map(session?.speakers, (speaker, index) => (
55 |
56 |
57 | {speaker?.name}
58 | {index < size(session?.speakers) - 1 ? ', ' : ''}
59 |
60 | {speaker?.company && | {speaker?.company}}
61 |
62 | ))}
63 |
64 | >
65 | )}
66 |
67 |
68 |
69 | );
70 | };
71 |
72 | const Overlay = styled(Dialog.Overlay, {
73 | base: {
74 | position: 'fixed',
75 | inset: 0,
76 | zIndex: 1,
77 | backgroundColor: 'rgba(0, 0, 0, 0.5)',
78 | },
79 | });
80 |
81 | const Content = styled(Dialog.Content, {
82 | base: {
83 | position: 'fixed',
84 | top: '50%',
85 | left: '50%',
86 | transform: 'translate(-50%, -50%)',
87 | width: 'calc(100% - 40px)',
88 | maxWidth: 595,
89 | borderRadius: {
90 | base: '10px',
91 | xl: '20px',
92 | },
93 | padding: {
94 | base: '20px',
95 | xl: '30px 40px 40px 40px',
96 | },
97 | backgroundColor: '#1A1B23',
98 | zIndex: 2,
99 | },
100 | });
101 |
102 | const Header = styled('header', {
103 | base: {
104 | display: 'flex',
105 | alignItems: 'center',
106 | justifyContent: 'space-between',
107 | },
108 | });
109 |
110 | const Time = styled('div', {
111 | base: {
112 | padding: '10px 12px',
113 | display: 'inline-flex',
114 | alignItems: 'center',
115 | gap: '10px',
116 | border: '1px solid rgba(168, 172, 192, 0.10)',
117 | borderRadius: 8,
118 | color: '#A8ACC0',
119 | fontSize: 12,
120 | fontStyle: 'normal',
121 | fontWeight: 500,
122 | },
123 | });
124 |
125 | const Title = styled(Dialog.Title, {
126 | base: {
127 | color: '#FFF',
128 | fontSize: {
129 | base: '18px',
130 | xl: '32px',
131 | },
132 | fontStyle: 'normal',
133 | fontWeight: 600,
134 | lineHeight: '140%',
135 | marginTop: {
136 | base: '20px',
137 | xl: '24px',
138 | },
139 | },
140 | });
141 |
142 | const Description = styled('p', {
143 | base: {
144 | color: '#FFF',
145 | fontSize: '14px',
146 | fontStyle: 'normal',
147 | fontWeight: 400,
148 | lineHeight: '160%',
149 | marginTop: {
150 | base: '12px',
151 | xl: '14px',
152 | },
153 | },
154 | });
155 |
156 | const Speaker = styled('div', {
157 | base: {
158 | color: '#A8ACC0',
159 | fontSize: '14px',
160 | fontStyle: 'normal',
161 | fontWeight: 500,
162 | marginTop: {
163 | base: '20px',
164 | xl: '30px',
165 | },
166 | },
167 | });
168 |
169 | const CloseButton = styled(CloseIcon, {
170 | base: {
171 | cursor: 'pointer',
172 | },
173 | });
174 |
175 | export default SessionModal;
176 |
--------------------------------------------------------------------------------
/src/app/components/ProgramSection/components/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ArrowIcon } from './ArrowIcon';
2 | export { default as ClockIcon } from './ClockIcon';
3 | export { default as SessionModal } from './SessionModal';
4 |
--------------------------------------------------------------------------------
/src/app/components/ProgramSection/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ProgramHeader } from './ProgramHeader';
2 | export { default as ProgramTab } from './ProgramTab';
3 | export { default as SessionList } from './SessionList';
4 |
--------------------------------------------------------------------------------
/src/app/components/ProgramSection/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './ProgramSection';
2 |
--------------------------------------------------------------------------------
/src/app/components/SponsorSection/SponsorSection.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { FC } from 'react';
4 | import { styled } from '@styled-system/jsx';
5 | import { Column, FadeIn, SectionTitle } from '~/shared/components';
6 |
7 | import { SponsorInfo } from './comopnents';
8 | import { motion, Variants } from 'framer-motion';
9 | import { useInView } from 'react-intersection-observer';
10 |
11 | import mediaSponsorLogo from './assets/it.png';
12 |
13 | const container: Variants = {
14 | visible: {
15 | transition: {
16 | staggerChildren: 0.15,
17 | delayChildren: 0.3,
18 | },
19 | },
20 | };
21 |
22 | const SponsorSection: FC = () => {
23 | const { ref, inView } = useInView({
24 | triggerOnce: true,
25 | });
26 | return (
27 |
32 |
33 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | Media Partner
46 |
47 |
48 |
49 |
50 |
51 |
52 | );
53 | };
54 |
55 | const Section = styled(motion.section, {
56 | base: {
57 | position: 'relative',
58 | padding: {
59 | base: '120px 0 60px 0',
60 | xl: '150px 0',
61 | },
62 | },
63 | });
64 |
65 | const SponsorList = styled('div', {
66 | base: {
67 | display: 'flex',
68 | flexDirection: 'column',
69 | width: {
70 | base: '100%',
71 | xl: '920px',
72 | },
73 | padding: {
74 | base: '20px',
75 | },
76 | gap: {
77 | base: '20px',
78 | xl: '50px',
79 | },
80 | marginTop: {
81 | base: '50px',
82 | xl: '100px',
83 | },
84 | },
85 | });
86 |
87 | const MediaSponsor = styled(FadeIn, {
88 | base: {
89 | display: 'flex',
90 | alignItems: 'center',
91 | gap: {
92 | base: '12px',
93 | xl: '24px',
94 | },
95 | marginTop: {
96 | base: '20px',
97 | xl: '50px',
98 | },
99 | height: {
100 | base: '25px',
101 | xl: '25px',
102 | },
103 | },
104 | });
105 |
106 | const Line = styled('div', {
107 | base: {
108 | flex: '1',
109 | height: '1px',
110 | backgroundColor: 'rgba(255, 255, 255, 0.2)',
111 | },
112 | });
113 |
114 | const Title = styled('a', {
115 | base: {
116 | display: 'flex',
117 | alignItems: 'center',
118 | fontSize: {
119 | base: '14px',
120 | xl: '18px',
121 | },
122 | gap: {
123 | base: '14px',
124 | xl: '24px',
125 | },
126 | cursor: 'pointer',
127 | color: 'rgba(255, 255, 255, 0.3)',
128 | },
129 | });
130 |
131 | const MediaLogo = styled('img', {
132 | base: {
133 | width: {
134 | base: '68px',
135 | xl: '102px',
136 | },
137 | height: {
138 | base: '17px',
139 | xl: '25px',
140 | },
141 | },
142 | });
143 |
144 | export default SponsorSection;
145 |
--------------------------------------------------------------------------------
/src/app/components/SponsorSection/assets/it.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/SponsorSection/assets/it.png
--------------------------------------------------------------------------------
/src/app/components/SponsorSection/comopnents/SponsorInfo.tsx:
--------------------------------------------------------------------------------
1 | import { map } from 'lodash-es';
2 | import { FC } from 'react';
3 | import { styled } from '@styled-system/jsx';
4 | import { FadeIn } from '~/shared/components';
5 |
6 | import googlecloudImage from './assets/googlecloud.png';
7 | import soomgoImage from './assets/soomgo.png';
8 | import wooritechImage from './assets/wooritech.png';
9 | import wyyyesImage from './assets/wyyyes.png';
10 | import imwebImage from './assets/imweb.png';
11 | import carrotImage from './assets/carrot.png';
12 | import oliveyoungImage from './assets/oliveyoung.png';
13 | import tossImage from './assets/toss.png';
14 | import stibeeImage from './assets/stibee.png';
15 | import fLabImage from './assets/F-Lab.png';
16 |
17 | type Grade = 'Master' | 'Diamond' | 'Platiunum' | 'Rookie';
18 | type SponsorData = {
19 | imageSrc: string;
20 | href: string;
21 | };
22 |
23 | const googlecloud: SponsorData = {
24 | imageSrc: googlecloudImage.src,
25 | href: 'https://cloud.google.com/',
26 | };
27 | const soomgo: SponsorData = {
28 | imageSrc: soomgoImage.src,
29 | href: 'https://www.soomgo.team/',
30 | };
31 | const wooritech: SponsorData = {
32 | imageSrc: wooritechImage.src,
33 | href: 'https://realgrid.com/',
34 | };
35 | const wyyyes: SponsorData = {
36 | imageSrc: wyyyesImage.src,
37 | href: 'https://wyyyes.career.greetinghr.com/o/111768',
38 | };
39 | const imweb: SponsorData = {
40 | imageSrc: imwebImage.src,
41 | href: 'https://team.imweb.me/',
42 | };
43 | const carrot: SponsorData = {
44 | imageSrc: carrotImage.src,
45 | href: 'https://about.daangn.com/',
46 | };
47 | const oliveyoung: SponsorData = {
48 | imageSrc: oliveyoungImage.src,
49 | href: 'https://career.oliveyoung.com/',
50 | };
51 | const toss: SponsorData = {
52 | imageSrc: tossImage.src,
53 | href: 'https://toss.im/career',
54 | };
55 | const stibee: SponsorData = {
56 | imageSrc: stibeeImage.src,
57 | href: 'https://stibee.com/',
58 | };
59 | const fLab: SponsorData = {
60 | imageSrc: fLabImage.src,
61 | href: 'https://f-lab.kr/',
62 | };
63 |
64 | const masterList = [googlecloud];
65 | const diamondList = [soomgo, wooritech];
66 | const platinumList = [wyyyes, imweb, oliveyoung, carrot, toss];
67 | const rookieList = [stibee, fLab];
68 | const sponsorListLookup: Record = {
69 | Master: masterList,
70 | Diamond: diamondList,
71 | Platiunum: platinumList,
72 | Rookie: rookieList,
73 | };
74 |
75 | interface Props {
76 | grade: Grade;
77 | }
78 |
79 | const SponsorInfo: FC = ({ grade }) => {
80 | const list = sponsorListLookup[grade];
81 | return (
82 |
83 | {grade}
84 |
85 | {map(list, ({ imageSrc, href }) => (
86 |
87 |
88 |
89 |
90 |
91 | ))}
92 |
93 |
94 | );
95 | };
96 |
97 | const Container = styled(FadeIn, {
98 | base: {
99 | display: 'flex',
100 | flexDirection: 'column',
101 | alignItems: 'center',
102 | width: '100%',
103 | height: {
104 | base: 'initial',
105 | },
106 | minHeight: {
107 | base: '144px',
108 | xl: '230px',
109 | },
110 | padding: {
111 | base: '20px 25px 30px 25px',
112 | xl: '30px 25px 28px 25px',
113 | },
114 | borderRadius: {
115 | base: '10px',
116 | xl: '20px',
117 | },
118 | background: 'rgba(78, 77, 96, 0.2)',
119 | },
120 | });
121 |
122 | const Grade = styled('h2', {
123 | base: {
124 | fontSize: {
125 | base: '16px',
126 | xl: '18px',
127 | },
128 | fontWeight: '500',
129 | color: 'rgba(255, 255, 255, 0.3)',
130 | lineHeight: '1.4',
131 | },
132 | });
133 |
134 | const List = styled('ul', {
135 | base: {
136 | display: 'flex',
137 | flexWrap: 'wrap',
138 | justifyContent: 'center',
139 | gap: {
140 | base: '10px',
141 | xl: '20px',
142 | },
143 | marginTop: {
144 | base: '20px',
145 | xl: '33px',
146 | },
147 | },
148 | });
149 |
150 | const Link = styled('a', {
151 | base: {
152 | display: 'block',
153 | cursor: 'pointer',
154 | borderRadius: {
155 | base: '10px',
156 | xl: '15px',
157 | },
158 | _hover: {
159 | backgroundColor: 'rgba(59, 59, 79, 0.3)',
160 | },
161 | },
162 | });
163 |
164 | const SponsorImage = styled('img', {
165 | variants: {
166 | grade: {
167 | Master: {
168 | height: {
169 | base: '52px',
170 | xl: '114px',
171 | },
172 | },
173 | Diamond: {
174 | height: {
175 | base: '64px',
176 | xl: '100px',
177 | },
178 | },
179 | Platiunum: {
180 | height: {
181 | base: '52px',
182 | xl: '70px',
183 | },
184 | },
185 | Rookie: {
186 | height: {
187 | base: '52px',
188 | xl: '70px',
189 | },
190 | },
191 | },
192 | },
193 | });
194 |
195 | export default SponsorInfo;
196 |
--------------------------------------------------------------------------------
/src/app/components/SponsorSection/comopnents/assets/F-Lab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/SponsorSection/comopnents/assets/F-Lab.png
--------------------------------------------------------------------------------
/src/app/components/SponsorSection/comopnents/assets/carrot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/SponsorSection/comopnents/assets/carrot.png
--------------------------------------------------------------------------------
/src/app/components/SponsorSection/comopnents/assets/googlecloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/SponsorSection/comopnents/assets/googlecloud.png
--------------------------------------------------------------------------------
/src/app/components/SponsorSection/comopnents/assets/imweb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/SponsorSection/comopnents/assets/imweb.png
--------------------------------------------------------------------------------
/src/app/components/SponsorSection/comopnents/assets/oliveyoung.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/SponsorSection/comopnents/assets/oliveyoung.png
--------------------------------------------------------------------------------
/src/app/components/SponsorSection/comopnents/assets/soomgo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/SponsorSection/comopnents/assets/soomgo.png
--------------------------------------------------------------------------------
/src/app/components/SponsorSection/comopnents/assets/stibee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/SponsorSection/comopnents/assets/stibee.png
--------------------------------------------------------------------------------
/src/app/components/SponsorSection/comopnents/assets/toss.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/SponsorSection/comopnents/assets/toss.png
--------------------------------------------------------------------------------
/src/app/components/SponsorSection/comopnents/assets/wooritech.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/SponsorSection/comopnents/assets/wooritech.png
--------------------------------------------------------------------------------
/src/app/components/SponsorSection/comopnents/assets/wyyyes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/app/components/SponsorSection/comopnents/assets/wyyyes.png
--------------------------------------------------------------------------------
/src/app/components/SponsorSection/comopnents/index.ts:
--------------------------------------------------------------------------------
1 | export { default as SponsorInfo } from './SponsorInfo';
2 |
--------------------------------------------------------------------------------
/src/app/components/SponsorSection/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './SponsorSection';
2 |
--------------------------------------------------------------------------------
/src/app/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as HeroIntroWrap } from './HeroIntroWrap';
2 | export { default as SponsorSection } from './SponsorSection';
3 | export { default as ProgramSection } from './ProgramSection';
4 | export { default as ChildcareSection } from './ChildcareSection';
5 | export { default as OpenSourceSection } from './OpenSourceSection';
6 | export { default as CoCSection } from './CoCSection';
7 | export { default as ContactSection } from './ContactSection';
8 | export { default as FooterSection } from './FooterSection';
9 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | html,
6 | body {
7 | max-width: 100vw;
8 | overflow-x: hidden;
9 | overflow-wrap: break-word;
10 | word-break: keep-all;
11 | }
12 |
13 | body {
14 | background-color: #010308;
15 | }
16 |
17 | a {
18 | text-decoration: none;
19 | }
20 |
21 | @media (prefers-color-scheme: dark) {
22 | html {
23 | color-scheme: dark;
24 | }
25 | }
26 |
27 | @layer reset, base, tokens, recipes, utilities;
28 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 | import { Inter } from 'next/font/google';
3 |
4 | import './reset.css';
5 | import './globals.css';
6 | import { PropsWithChildren } from 'react';
7 |
8 | const inter = Inter({ subsets: ['latin'] });
9 |
10 | export const metadata: Metadata = {
11 | title: 'FEConf 2024',
12 | description:
13 | '국내 최대 프론트엔드 개발 컨퍼런스, FECONF 2024가 8월 24일 오프라인으로 찾아옵니다.',
14 | openGraph: {
15 | type: 'website',
16 | url: 'https://2024.feconf.kr',
17 | images: [
18 | {
19 | url: 'https://2024.feconf.kr/fe2024_og.png',
20 | width: 1200,
21 | height: 630,
22 | alt: 'FEConf 2024',
23 | },
24 | ],
25 | },
26 | twitter: {
27 | title: 'FEConf 2024',
28 | description:
29 | '국내 최대 프론트엔드 개발 컨퍼런스, FECONF 2024가 8월 24일 오프라인으로 찾아옵니다.',
30 | card: 'summary_large_image',
31 | creator: '@FeConf',
32 | images: [{ url: 'https://2024.feconf.kr/fe2024_og.png' }],
33 | },
34 | };
35 |
36 | function RootLayout({ children }: Readonly) {
37 | return (
38 |
39 |
40 |
45 |
49 |
59 |
60 | {children}
61 |
62 | );
63 | }
64 |
65 | export default RootLayout;
66 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import {
4 | ChildcareSection,
5 | CoCSection,
6 | ContactSection,
7 | FooterSection,
8 | HeroIntroWrap,
9 | OpenSourceSection,
10 | ProgramSection,
11 | SponsorSection,
12 | } from './components';
13 | import { Header, Footer } from '~/shared/components';
14 | import { AuroraProvider } from '~/features/aurora/contexts';
15 | import { AuroraContainer } from '~/features/effects/components';
16 |
17 | import { styled } from '@styled-system/jsx';
18 |
19 | function Home() {
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
39 | const Main = styled('main', {});
40 |
41 | export default Home;
42 |
--------------------------------------------------------------------------------
/src/app/reset.css:
--------------------------------------------------------------------------------
1 | /***
2 | The new CSS reset - version 1.11.2 (last updated 15.11.2023)
3 | GitHub page: https://github.com/elad2412/the-new-css-reset
4 | ***/
5 |
6 | /*
7 | Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property
8 | - The "symbol *" part is to solve Firefox SVG sprite bug
9 | - The "html" element is excluded, otherwise a bug in Chrome breaks the CSS hyphens property (https://github.com/elad2412/the-new-css-reset/issues/36)
10 | */
11 |
12 | @layer priority999 {
13 | *:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)) {
14 | all: unset;
15 | display: revert;
16 | }
17 |
18 | /* Preferred box-sizing value */
19 | *,
20 | *::before,
21 | *::after {
22 | box-sizing: border-box;
23 | }
24 |
25 | /* Fix mobile Safari increase font-size on landscape mode */
26 | html {
27 | -moz-text-size-adjust: none;
28 | -webkit-text-size-adjust: none;
29 | text-size-adjust: none;
30 | color: white;
31 | }
32 |
33 | /* Remove list styles (bullets/numbers) */
34 | ol, ul, menu, summary {
35 | list-style: none;
36 | }
37 |
38 | /* For images to not be able to exceed their container */
39 | img {
40 | max-inline-size: 100%;
41 | max-block-size: 100%;
42 | }
43 |
44 | /* removes spacing between cells in tables */
45 | table {
46 | border-collapse: collapse;
47 | }
48 |
49 | /* Safari - solving issue when using user-select:none on the text input doesn't working */
50 | input, textarea {
51 | -webkit-user-select: auto;
52 | }
53 |
54 | /* revert the 'white-space' property for textarea elements on Safari */
55 | textarea {
56 | white-space: revert;
57 | }
58 |
59 | /* minimum style to allow to style meter element */
60 | meter {
61 | -webkit-appearance: revert;
62 | appearance: revert;
63 | }
64 |
65 | /* preformatted text - use only for this feature */
66 | :where(pre) {
67 | all: revert;
68 | box-sizing: border-box;
69 | }
70 |
71 | /* reset default text opacity of input placeholder */
72 | ::placeholder {
73 | color: unset;
74 | }
75 |
76 | /* fix the feature of 'hidden' attribute.
77 | display:revert; revert to element instead of attribute */
78 | :where([hidden]) {
79 | display: none;
80 | }
81 |
82 | /* revert for bug in Chromium browsers
83 | - fix for the content editable attribute will work properly.
84 | - webkit-user-select: auto; added for Safari in case of using user-select:none on wrapper element*/
85 | :where([contenteditable]:not([contenteditable="false"])) {
86 | -moz-user-modify: read-write;
87 | -webkit-user-modify: read-write;
88 | overflow-wrap: break-word;
89 | -webkit-line-break: after-white-space;
90 | -webkit-user-select: auto;
91 | }
92 |
93 | /* apply back the draggable feature - exist only in Chromium and Safari */
94 | :where([draggable="true"]) {
95 | -webkit-user-drag: element;
96 | }
97 |
98 | /* Revert Modal native behavior */
99 | :where(dialog:modal) {
100 | all: revert;
101 | box-sizing: border-box;
102 | }
103 |
104 | /* Remove details summary webkit styles */
105 | ::-webkit-details-marker {
106 | display: none;
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/features/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/features/.gitkeep
--------------------------------------------------------------------------------
/src/features/aurora/assets/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/features/aurora/assets/background.png
--------------------------------------------------------------------------------
/src/features/aurora/assets/background2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/features/aurora/assets/background2.png
--------------------------------------------------------------------------------
/src/features/aurora/assets/background3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/features/aurora/assets/background3.png
--------------------------------------------------------------------------------
/src/features/aurora/assets/background4-half.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/features/aurora/assets/background4-half.png
--------------------------------------------------------------------------------
/src/features/aurora/assets/background4-min.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/features/aurora/assets/background4-min.png
--------------------------------------------------------------------------------
/src/features/aurora/assets/background4-tiny.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/features/aurora/assets/background4-tiny.png
--------------------------------------------------------------------------------
/src/features/aurora/assets/background4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/features/aurora/assets/background4.png
--------------------------------------------------------------------------------
/src/features/aurora/assets/bg2-tiny.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/features/aurora/assets/bg2-tiny.png
--------------------------------------------------------------------------------
/src/features/aurora/components/Aurora.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | 'use client';
3 |
4 | import { useThree } from '@react-three/fiber';
5 | import { useTexture } from '@react-three/drei';
6 | import { FC, useCallback, useEffect, useRef, useState } from 'react';
7 | import { Mesh, ShaderMaterial, Vector2 } from 'three';
8 | import type { GUI as GUIType } from 'dat.gui';
9 |
10 | import auroraImage from '../assets/background4-half.png';
11 | import { useHeroScreen } from '~/features/hooks/useHeroScreen';
12 | import { usePrefersReducedMotionRef } from '~/features/hooks/usePrefersReducedMotion';
13 |
14 | let GUI: typeof GUIType;
15 |
16 | if (typeof window !== 'undefined') {
17 | import('dat.gui')
18 | .then(module => {
19 | GUI = module.default.GUI;
20 | })
21 | .catch(error => console.error('Failed to load dat.GUI:', error));
22 | }
23 |
24 | const vertexShader = `
25 | vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
26 | vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
27 | float snoise(vec3 v) {
28 | const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
29 | const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
30 |
31 | // First corner
32 | vec3 i = floor(v + dot(v, C.yyy) );
33 | vec3 x0 = v - i + dot(i, C.xxx) ;
34 |
35 | // Other corners
36 | vec3 g = step(x0.yzx, x0.xyz);
37 | vec3 l = 1.0 - g;
38 | vec3 i1 = min( g.xyz, l.zxy );
39 | vec3 i2 = max( g.xyz, l.zxy );
40 |
41 | // x0 = x0 - 0. + 0.0 * C
42 | vec3 x1 = x0 - i1 + 1.0 * C.xxx;
43 | vec3 x2 = x0 - i2 + 2.0 * C.xxx;
44 | vec3 x3 = x0 - 1. + 3.0 * C.xxx;
45 |
46 | // Permutations
47 | i = mod(i, 289.0 );
48 | vec4 p = permute( permute( permute(
49 | i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
50 | + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
51 | + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
52 |
53 | // Gradients
54 | // ( N*N points uniformly over a square, mapped onto an octahedron.)
55 | float n_ = 1.0/7.0; // N=7
56 | vec3 ns = n_ * D.wyz - D.xzx;
57 |
58 | vec4 j = p - 49.0 * floor(p * ns.z *ns.z); // mod(p,N*N)
59 |
60 | vec4 x_ = floor(j * ns.z);
61 | vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N)
62 |
63 | vec4 x = x_ *ns.x + ns.yyyy;
64 | vec4 y = y_ *ns.x + ns.yyyy;
65 | vec4 h = 1.0 - abs(x) - abs(y);
66 |
67 | vec4 b0 = vec4( x.xy, y.xy );
68 | vec4 b1 = vec4( x.zw, y.zw );
69 |
70 | vec4 s0 = floor(b0)*2.0 + 1.0;
71 | vec4 s1 = floor(b1)*2.0 + 1.0;
72 | vec4 sh = -step(h, vec4(0.0));
73 |
74 | vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
75 | vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
76 |
77 | vec3 p0 = vec3(a0.xy,h.x);
78 | vec3 p1 = vec3(a0.zw,h.y);
79 | vec3 p2 = vec3(a1.xy,h.z);
80 | vec3 p3 = vec3(a1.zw,h.w);
81 |
82 | //Normalise gradients
83 | vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
84 | p0 *= norm.x;
85 | p1 *= norm.y;
86 | p2 *= norm.z;
87 | p3 *= norm.w;
88 |
89 | // Mix final noise value
90 | vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
91 | m = m * m;
92 | return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
93 | dot(p2,x2), dot(p3,x3) ) );
94 | }
95 |
96 | mat4 rotateZ(float angle) {
97 | float c = cos(angle);
98 | float s = sin(angle);
99 | return mat4(c, -s, 0.0, 0.0,
100 | s, c, 0.0, 0.0,
101 | 0.0, 0.0, 1.0, 0.0,
102 | 0.0, 0.0, 0.0, 1.0);
103 | }
104 |
105 | uniform float uTime;
106 | uniform float uTurbulence;
107 | uniform float uFrequency;
108 | uniform float uRotationSpeed;
109 |
110 | varying vec2 vUv;
111 | varying vec3 vColor;
112 |
113 | void main() {
114 | vColor = vec3(0.5);
115 |
116 | vUv = uv;
117 |
118 | mat4 rotationMatrix = rotateZ(uRotationSpeed + (uTime * 0.2));
119 | float noise = snoise(vec3(uv, uTime * uFrequency));
120 |
121 | // vec3 pos = vec3(position.x, position.y, position.z + 0.1 * sin(uv.x * uTurbulence) * sin(uv.y * uTurbulence));
122 | vec3 pos = vec3(position.x, position.y, position.z + noise * uTurbulence);
123 | // vec3 pos = vec3(position.x, position.y, position.z);
124 |
125 | gl_Position = projectionMatrix * modelViewMatrix * rotationMatrix * vec4(pos, 1.0);
126 | }
127 | `;
128 |
129 | const fragmentShader = `
130 | uniform vec2 u_resolution;
131 | uniform sampler2D aoMap;
132 | uniform float uTime;
133 |
134 | varying vec2 vUv;
135 |
136 | void main() {
137 | vec2 uv = vUv;
138 | gl_FragColor = texture2D(aoMap, uv);
139 | }
140 | `;
141 |
142 | interface Props {}
143 |
144 | const Aurora: FC = () => {
145 | const meshRef = useRef(null);
146 | const shaderRef = useRef(null);
147 | const background = useTexture(auroraImage.src);
148 | const textureRatio = 1; // height / width
149 | const [guiState, setGuiState] = useState({
150 | // meshSize: 35,
151 | meshSize: 52,
152 | turbulence: 3,
153 | frequency: 0.3,
154 | rotationSpeed: 0.2,
155 | yPosition: 13.5,
156 | wireframe: false,
157 | });
158 | const { gl, scene, camera } = useThree();
159 | const { scrollRef, screenSizeRef, sizeRef, onScroll, onResize, dispatch } =
160 | useHeroScreen();
161 |
162 | const startTime = Date.now();
163 | let id = 0;
164 |
165 | const render = useCallback((once?: boolean) => {
166 | cancelAnimationFrame(id);
167 | if (shaderRef.current?.uniforms) {
168 | shaderRef.current!.uniforms.uTime.value = reducedRef.current
169 | ? 0
170 | : (Date.now() - startTime) / 1000;
171 | }
172 | gl.render(scene, camera);
173 |
174 | if (!once && !reducedRef.current) {
175 | id = requestAnimationFrame(() => render());
176 | }
177 | }, []);
178 |
179 | useEffect(() => {
180 | const gui = new GUI();
181 | const handleChange = () => setGuiState({ ...guiState });
182 | gui.add(guiState, 'wireframe').onChange(handleChange);
183 | gui.add(guiState, 'meshSize', 5, 100).onChange(handleChange);
184 | gui.add(guiState, 'frequency', 0, 1).onChange(handleChange);
185 | gui.add(guiState, 'turbulence', 0, 5).onChange(handleChange);
186 | gui.add(guiState, 'yPosition', 0, 15, 0.1).onChange(handleChange);
187 | gui.add(guiState, 'rotationSpeed', 0, 10, 0.1).onChange(handleChange);
188 | gui.domElement.style.marginTop = '70px';
189 | gui.hide();
190 | return () => {
191 | gui.destroy();
192 | };
193 | }, [setGuiState, guiState]);
194 |
195 | useEffect(() => {
196 | onResize(() => {
197 | if (!gl) {
198 | return;
199 | }
200 | const { width, height } = sizeRef.current;
201 | gl.setSize(width, height);
202 | camera.position.z = 6;
203 | (camera as any).aspect = width / height;
204 | camera.updateProjectionMatrix();
205 | dispatch('scroll');
206 | });
207 | onScroll(() => {
208 | if (!gl) {
209 | return;
210 | }
211 | const firstSectionRect = screenSizeRef.current[0];
212 | const secondSectionRect = screenSizeRef.current[1];
213 | const maxScaleScroll =
214 | firstSectionRect.height +
215 | secondSectionRect.height * 0.66 -
216 | sizeRef.current.height / 2;
217 | const prevVisible = meshRef.current!.visible;
218 |
219 | const opacity = Math.min(
220 | Math.max(0, scrollRef.current - maxScaleScroll) * 0.002,
221 | 1
222 | );
223 |
224 | gl.domElement.style.opacity = `${opacity}`;
225 | meshRef.current!.visible = opacity > 0;
226 |
227 | if (opacity || prevVisible !== !!opacity) {
228 | render(!opacity);
229 | }
230 | });
231 |
232 | dispatch('resize');
233 | }, [gl, camera]);
234 | const reducedRef = usePrefersReducedMotionRef(() => {
235 | render(meshRef.current!.visible);
236 | });
237 |
238 | return (
239 | <>
240 |
246 |
249 |
274 |
275 | >
276 | );
277 | };
278 |
279 | export default Aurora;
280 |
--------------------------------------------------------------------------------
/src/features/aurora/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Aurora } from './Aurora';
2 |
--------------------------------------------------------------------------------
/src/features/aurora/contexts/AuroraContext.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import {
4 | createContext,
5 | FC,
6 | PropsWithChildren,
7 | useCallback,
8 | useContext,
9 | useState,
10 | } from 'react';
11 | import { noop } from 'lodash-es';
12 |
13 | interface AuroraContextType {
14 | visible: boolean;
15 | show: () => void;
16 | hide: () => void;
17 | }
18 |
19 | const AuroraContext = createContext({
20 | visible: false,
21 | show: noop,
22 | hide: noop,
23 | });
24 |
25 | const AuroraProvider: FC = ({ children }) => {
26 | const [visible, setVisible] = useState(false);
27 | const show = useCallback(() => setVisible(true), []);
28 | const hide = useCallback(() => setVisible(false), []);
29 | return (
30 |
31 | {children}
32 |
33 | );
34 | };
35 |
36 | const useAurora = () => useContext(AuroraContext);
37 |
38 | export default AuroraContext;
39 | export { useAurora, AuroraProvider };
40 |
--------------------------------------------------------------------------------
/src/features/aurora/contexts/index.ts:
--------------------------------------------------------------------------------
1 | export { AuroraProvider, useAurora } from './AuroraContext';
2 |
--------------------------------------------------------------------------------
/src/features/effects/components/AuroraContainer.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { FC, PropsWithChildren } from 'react';
4 | import { Canvas as FiberCanvas } from '@react-three/fiber';
5 | import { styled } from '@styled-system/jsx';
6 | import Aurora from '~/features/aurora/components/Aurora';
7 |
8 | interface Props {}
9 |
10 | export const AuroraContainer: FC> = () => {
11 | return (
12 |
13 |
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | const Container = styled('div', {
26 | base: {
27 | position: 'fixed',
28 | top: '0',
29 | left: '0',
30 | width: '100%',
31 | height: '100%',
32 | display: 'flex',
33 | justifyContent: 'center',
34 | zIndex: '-1',
35 | },
36 | });
37 |
--------------------------------------------------------------------------------
/src/features/effects/components/SphereContainer.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { FC, PropsWithChildren } from 'react';
4 | import { Canvas as FiberCanvas } from '@react-three/fiber';
5 | import { styled } from '@styled-system/jsx';
6 | import { GradientSphere } from './SphereEffect/components';
7 |
8 | interface Props {}
9 |
10 | export const SphereContainer: FC> = () => {
11 | return (
12 |
13 |
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | const Container = styled('div', {
28 | base: {
29 | position: 'absolute',
30 | top: '-480px',
31 | left: '0',
32 | width: '100vw',
33 | height: 'calc(100% + 800px)',
34 | display: 'flex',
35 | justifyContent: 'center',
36 | zIndex: '-1',
37 | },
38 | });
39 |
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/SphereEffect.tsx:
--------------------------------------------------------------------------------
1 | 'use client'; // This is a client component 👈🏽
2 |
3 | import { FC, useEffect, useRef } from 'react';
4 | import { useThree } from '@react-three/fiber';
5 | import { OrthographicCamera } from 'three';
6 | import { GradientSphere } from './components';
7 |
8 | interface Props {}
9 |
10 | const Camera: FC = () => {
11 | const { set } = useThree();
12 | const cameraRef = useRef(null);
13 |
14 | useEffect(() => {
15 | const handleResize = () => {
16 | if (!cameraRef.current) return;
17 | const { innerWidth, innerHeight } = window;
18 | const ratio = innerWidth / innerHeight;
19 | const height = 8;
20 | const width = height * ratio;
21 | cameraRef.current.left = width / -2;
22 | cameraRef.current.right = width / 2;
23 | cameraRef.current.top = height / 2;
24 | cameraRef.current.bottom = height / -2;
25 | cameraRef.current.near = 1;
26 | cameraRef.current.far = 100;
27 | cameraRef.current?.updateProjectionMatrix();
28 | };
29 | window.addEventListener('resize', handleResize, { passive: true });
30 | handleResize();
31 | set({ camera: cameraRef.current! });
32 | return () => {
33 | window.removeEventListener('resize', handleResize);
34 | };
35 | }, []);
36 |
37 | return ;
38 | };
39 |
40 | const SphereEffect: FC = () => {
41 | // const { opacity } = useSceneComposer();
42 | // const [loaded, setLoaded] = useState(false);
43 | // useEffect(() => {
44 | // const image = new Image();
45 | // image.src = noiseImage.src;
46 | // image.complete && setLoaded(true);
47 | // image.onload = () => setLoaded(true);
48 | // }, []);
49 | return (
50 | <>
51 |
52 |
53 | >
54 | );
55 | };
56 |
57 | // const BackgroundColor: FC = () => {
58 | // const { scene } = useThree();
59 | // useEffect(() => {
60 | // scene.background = new Color('#16171A');
61 | // }, [scene]);
62 | // return null;
63 | // };
64 |
65 | export default SphereEffect;
66 |
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/assets/img_noise.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/features/effects/components/SphereEffect/assets/img_noise.png
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/components/GradientSphere/ExtendedMaterial.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/leoncvlt/three-extended-material
2 | /* eslint-disable */
3 | const defaultExtension = {
4 | name: '',
5 | uniforms: {},
6 | defines: {},
7 | vertexShader: (shader: any) => shader,
8 | fragmentShader: (shader: any) => shader,
9 | };
10 |
11 | export function ExtendedMaterial(
12 | SuperMaterial: any,
13 | extensions: any[] = [],
14 | properties: any = {},
15 | options = { debug: false, programCacheKey: null }
16 | ) {
17 | if (!new.target) {
18 | throw 'Uncaught TypeError: Class constructor ExtendedMaterial cannot be invoked without "new"';
19 | }
20 |
21 | class _ExtendedMaterial extends SuperMaterial {
22 | constructor(props: any = {}) {
23 | super();
24 |
25 | if (!Array.isArray(extensions)) {
26 | extensions = [extensions];
27 | }
28 |
29 | // sanitize all extensions by adding empty fields if necessary
30 | extensions = extensions.map((extension) => ({
31 | ...defaultExtension,
32 | ...extension,
33 | }));
34 |
35 | this.uniforms = {};
36 |
37 | // hash the supermaterial name alongside with the extensions' ids
38 | // to generate a gl program cache key for this combination
39 | this._id = SuperMaterial.name + '_' + extensions.map((e) => e.name).join('_');
40 | this._cacheKey = 0;
41 | for (let i = 0; i < this._id.length; i++) {
42 | this._cacheKey = (Math.imul(31, this._cacheKey) + this._id.charCodeAt(i)) | 0;
43 | }
44 |
45 | // go through each extension's uniforms
46 | // and add getters and setters to the material
47 | extensions.forEach((extension) => {
48 | Object.entries(extension.uniforms).forEach(([uniform, value]) => {
49 | if (uniform in this.uniforms) {
50 | console.warn(
51 | `ExtendedMaterial: duplicated '${uniform}' uniform - shader compilation might fail.` +
52 | `To fix this, rename the ${uniform} uniform in the ${extension.name} extension.`
53 | );
54 | }
55 | this.uniforms[uniform] = { value: props[uniform] || value };
56 | if (uniform in this) {
57 | console.warn(
58 | `ExtendedMaterial: the material already contains a '${uniform}' property - ` +
59 | `getters and setters will not be set.` +
60 | `To fix this, rename the ${uniform} uniform in the ${extension.name} extension.`
61 | );
62 | } else {
63 | Object.defineProperty(this, uniform, {
64 | get() {
65 | return this.uniforms[uniform]?.value;
66 | },
67 | set(newValue) {
68 | if (this.uniforms) {
69 | this.uniforms[uniform].value = newValue;
70 | }
71 | },
72 | });
73 | }
74 | });
75 | });
76 |
77 | // set the initial material properties passed to the method
78 | this.setValues(props);
79 | }
80 |
81 | onBeforeCompile(shader: any) {
82 | extensions.forEach((extension) => {
83 | Object.keys(extension.uniforms).forEach((uniform) => {
84 | shader.uniforms[uniform] = this.uniforms[uniform];
85 | });
86 | Object.entries(extension.defines).forEach(([define, value]) => {
87 | if (!shader.defines) {
88 | shader.defines = {};
89 | }
90 | shader.defines[define] = value;
91 | });
92 |
93 | shader.vertexShader = extension.vertexShader(shader.vertexShader, shader.shaderType);
94 | shader.fragmentShader = extension.fragmentShader(shader.fragmentShader, shader.shaderType);
95 | });
96 |
97 | if (options.debug) {
98 | console.debug(this._id, shader);
99 | }
100 |
101 | this.uniforms = shader.uniforms;
102 | this.needsUpdate = true;
103 | }
104 |
105 | customProgramCacheKey() {
106 | return options.programCacheKey || this._cacheKey || 0;
107 | }
108 | }
109 |
110 | return new _ExtendedMaterial(properties);
111 | }
112 |
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/components/GradientSphere/GradientSphere.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-hooks/exhaustive-deps */
2 | /* eslint-disable @typescript-eslint/no-explicit-any */
3 | import {
4 | AmbientLight,
5 | Color,
6 | MathUtils,
7 | Mesh,
8 | MeshPhysicalMaterial,
9 | OrthographicCamera,
10 | ShaderMaterial,
11 | Vector2,
12 | } from 'three';
13 | import { useCallback, useEffect, useMemo, useRef } from 'react';
14 | import { useTexture } from '@react-three/drei';
15 |
16 | import gradientImage1 from './assets/gradient1.jpg';
17 | import { useThree } from '@react-three/fiber';
18 | import { MeshPhysicalMaterialWithGlow } from './MeshPhysicalMaterialWithGlow';
19 | // import { MeshPosition, useSceneComposer } from '~/shared/contexts';
20 | // import useMeshScale from '~/shared/hooks/useMeshScale';
21 | import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
22 | import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
23 | import { BokehPass, ShaderPass } from 'three/examples/jsm/Addons.js';
24 | import { BlurShader } from './external/BlurShader';
25 | import { useHeroScreen } from '~/features/hooks/useHeroScreen';
26 | import { usePrefersReducedMotionRef } from '~/features/hooks/usePrefersReducedMotion';
27 | import { BokehShader } from './external/BokehShader';
28 |
29 | const sphereSize = 1;
30 | const sphereSegments = 64;
31 |
32 | const glowState = {
33 | glowIntensity: 3,
34 | glowPower: 4,
35 | glowColor: '#5d9be5',
36 | visible: true,
37 | rotatable: true,
38 | };
39 |
40 | const dofState = {
41 | focus: 150,
42 | aperture: 20,
43 | maxblur: 0.02,
44 | blurOffset: 3,
45 | blurCount: 2,
46 | blurOpacity: 1,
47 | };
48 |
49 | function GradientSphere() {
50 | const meshRef = useRef(null);
51 | const materialRef = useRef(null);
52 | const texture = useTexture(gradientImage1.src, texture => {
53 | texture.repeat.setX(-1);
54 | texture.repeat.setY(1);
55 | });
56 |
57 | const lightRef = useRef(null);
58 | const processingRef = useRef>({});
59 | const three = useThree();
60 | const { gl, scene } = three;
61 | const camera = three.camera as OrthographicCamera;
62 |
63 | const { sizeRef, scrollRef, screenSizeRef, onScroll, onResize, dispatch } =
64 | useHeroScreen();
65 |
66 | const render = useCallback(() => {
67 | if (!meshRef.current?.visible) {
68 | return;
69 | }
70 | processingRef.current?.composer?.render();
71 | }, []);
72 |
73 | useEffect(() => {
74 | if (!gl || !scene) {
75 | processingRef.current = {};
76 | return;
77 | }
78 |
79 | const postprocessing = processingRef.current;
80 | const renderPass = new RenderPass(scene, camera);
81 |
82 | renderPass.clearAlpha = 1;
83 | renderPass.clearColor = new Color('#010308');
84 | renderPass.clearDepth = true;
85 |
86 | // focus?: number;
87 | // aspect?: number;
88 | // aperture?: number;
89 | // maxblur?: number;
90 | const bokehPass = new BokehPass(scene, camera, {
91 | focus: dofState.focus,
92 | aperture: dofState.aperture * 0.00001,
93 | maxblur: dofState.maxblur,
94 | });
95 |
96 | const composer = new EffectComposer(gl);
97 |
98 | composer.addPass(renderPass);
99 |
100 | const blurShaderPass = new ShaderPass(
101 | new ShaderMaterial({
102 | ...BlurShader,
103 | uniforms: {
104 | ...BlurShader.uniforms,
105 | },
106 | })
107 | );
108 | const bokehShaderPass = new ShaderPass(
109 | new ShaderMaterial({
110 | ...BokehShader,
111 | uniforms: {
112 | ...BokehShader.uniforms,
113 | },
114 | })
115 | );
116 | composer.addPass(blurShaderPass);
117 | composer.addPass(bokehShaderPass);
118 |
119 | postprocessing.composer = composer;
120 | postprocessing.bokeh = bokehPass;
121 | postprocessing.bokehShaderPass = bokehShaderPass;
122 | postprocessing.blurShaderPass = blurShaderPass;
123 | postprocessing.renderPass = renderPass;
124 |
125 | return () => {
126 | processingRef.current = {};
127 | };
128 | }, [gl, scene, camera]);
129 |
130 | useEffect(() => {
131 | if (!processingRef.current.bokeh) {
132 | return;
133 | }
134 | // const bokeh = processingRef.current.bokeh;
135 | // bokeh.uniforms['focus'].value = dofState.focus;
136 | // bokeh.uniforms['aperture'].value = dofState.aperture * 0.00001;
137 | // bokeh.uniforms['maxblur'].value = dofState.maxblur;
138 |
139 | const bokehShaderPass = processingRef.current.bokehShaderPass;
140 |
141 | bokehShaderPass.uniforms['focus'].value = dofState.focus;
142 | bokehShaderPass.uniforms['aperture'].value = dofState.aperture * 0.00001;
143 | bokehShaderPass.uniforms['maxblur'].value = dofState.maxblur;
144 |
145 | const blurOffset = dofState.blurOffset;
146 | const res = gl.getDrawingBufferSize(new Vector2());
147 |
148 | processingRef.current.blurShaderPass.uniforms.uBlurOffset.value =
149 | blurOffset / res.y;
150 | }, [gl, Object.values(dofState)]);
151 |
152 | useEffect(() => {
153 | meshRef.current!.rotation.x = MathUtils.degToRad(-50);
154 |
155 | let id = requestAnimationFrame(function raq() {
156 | if (!meshRef.current) {
157 | id = requestAnimationFrame(raq);
158 | return;
159 | }
160 |
161 | const targetRad = MathUtils.degToRad(-250);
162 |
163 | meshRef.current.rotation.x = MathUtils.lerp(
164 | meshRef.current.rotation.x,
165 | targetRad,
166 | 0.05
167 | );
168 |
169 | render();
170 | if (Math.abs(meshRef.current.rotation.x - targetRad) < 0.05) {
171 | return;
172 | }
173 | id = requestAnimationFrame(raq);
174 | });
175 |
176 | onResize(() => {
177 | if (!processingRef.current?.composer) {
178 | return;
179 | }
180 |
181 | const innerWidth = sizeRef.current.width;
182 | const innerHeight = gl.domElement.clientHeight; // sizeRef.current.height;
183 | const ratio = innerWidth / innerHeight;
184 | const height = 8;
185 | const width = height * ratio;
186 |
187 | camera.left = width / -2;
188 | camera.right = width / 2;
189 | camera.top = height / 2;
190 | camera.bottom = height / -2;
191 | camera.near = 1;
192 | camera.far = 100;
193 | camera.updateProjectionMatrix();
194 |
195 | gl.setSize(innerWidth, innerHeight);
196 | processingRef.current?.composer.setSize(innerWidth, innerHeight);
197 |
198 | dispatch('scroll');
199 | });
200 |
201 | onScroll(() => {
202 | if (!meshRef.current) {
203 | return;
204 | }
205 | const isReduced = reducedRef.current;
206 | const firstSectionRect = screenSizeRef.current[0];
207 | const secondSectionRect = screenSizeRef.current[1];
208 | const secondSectionScroll = firstSectionRect.height;
209 | const thirdScaleScroll =
210 | firstSectionRect.height +
211 | secondSectionRect.height -
212 | -sizeRef.current.height;
213 | const maxScaleScroll =
214 | secondSectionScroll +
215 | secondSectionRect.height / 3 -
216 | sizeRef.current.height / 2;
217 |
218 | const lightRGB = [255, 246, 206];
219 | const scaleDist = Math.min(1, scrollRef.current / maxScaleScroll);
220 | const scale = Math.max(1, 1 + scaleDist * 3);
221 | const emissiveValue = Math.max(0, scale / 2 - 1);
222 |
223 | processingRef.current.bokehShaderPass.enabled = !isReduced;
224 | lightRGB.forEach((color, i) => {
225 | lightRGB[i] = Math.min(255, color * scale);
226 | });
227 |
228 | materialRef.current!.emissive = new Color(
229 | emissiveValue,
230 | emissiveValue,
231 | emissiveValue
232 | );
233 | lightRef.current!.color = new Color(
234 | lightRGB[0] / 255,
235 | lightRGB[1] / 255,
236 | lightRGB[2] / 255
237 | );
238 |
239 | let scrollTop = scrollRef.current!;
240 |
241 | if (!isReduced && scrollTop > maxScaleScroll) {
242 | scrollTop += (scrollTop - maxScaleScroll) * 1.1;
243 | }
244 |
245 | const opacity = Math.min(
246 | 1,
247 | Math.max(0, 1 - (scrollTop - maxScaleScroll - 100) * 0.0004)
248 | );
249 |
250 | gl.domElement.style.opacity = `${opacity}`;
251 |
252 | const prevVisible = meshRef.current!.visible;
253 | const nextY = isReduced
254 | ? 0.3
255 | : 0.3 - (scrollRef.current / thirdScaleScroll) * 0.1;
256 | const ss = 1 + (scrollRef.current / thirdScaleScroll) * 0.1;
257 | const ms = 3.2 * (isReduced ? 1 : ss);
258 |
259 | meshRef.current!.scale.x = ms;
260 | meshRef.current!.scale.y = ms;
261 | meshRef.current!.scale.z = ms;
262 | meshRef.current!.position.y = nextY;
263 | meshRef.current!.position.z = -3;
264 |
265 | if (opacity) {
266 | meshRef.current!.visible = true;
267 | } else {
268 | meshRef.current!.visible = false;
269 | }
270 |
271 | if (opacity || prevVisible !== !!opacity) {
272 | render();
273 | }
274 | });
275 |
276 | dispatch('resize');
277 |
278 | return () => {
279 | cancelAnimationFrame(id);
280 | };
281 | }, []);
282 | const reducedRef = usePrefersReducedMotionRef(() => {
283 | dispatch('scroll');
284 | });
285 |
286 | const glowColor = useMemo(() => {
287 | return new Color(glowState.glowColor);
288 | }, [glowState.glowColor]);
289 |
290 | return (
291 | <>
292 | {/* */}
293 |
294 |
295 |
296 |
308 |
309 | >
310 | );
311 | }
312 |
313 | export default GradientSphere;
314 |
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/components/GradientSphere/MeshPhysicalMaterialWithGlow.tsx:
--------------------------------------------------------------------------------
1 | // https://github.com/leoncvlt/three-extended-material
2 | /* eslint-disable */
3 | import { Color, MeshPhysicalMaterial, Vector2 } from 'three';
4 | import { ReactExtendedMaterial } from './ReactExtendedMaterial';
5 | import { MeshPhysicalMaterialProps, useThree } from '@react-three/fiber';
6 | import { forwardRef, useEffect } from 'react';
7 |
8 | const RimGlow = {
9 | name: 'rim-glow',
10 | uniforms: {
11 | glowIntensity: 1,
12 | glowColor: new Color('#2E3FFF'),
13 | glowPower: 5,
14 | },
15 | vertexShader: (shader: any) => {
16 | if (!shader.includes('vViewPosition')) {
17 | shader = `
18 | varying vec3 vViewPosition;
19 | varying vec2 vUv;
20 |
21 | ${shader.replace(
22 | '#include ',
23 | /*glsl*/ `
24 | #include
25 | vUv = uv;
26 | vViewPosition = -mvPosition.xyz;
27 | `
28 | )}
29 | `;
30 | } else {
31 | shader = `
32 | varying vec2 vUv;
33 |
34 | ${shader.replace(
35 | '#include ',
36 | /*glsl*/ `
37 | #include
38 | vUv = uv;
39 | `
40 | )}
41 | `;
42 | }
43 |
44 | return shader;
45 | },
46 | fragmentShader: (shader: any) => {
47 | shader = `
48 | uniform float glowIntensity;
49 | uniform vec3 glowColor;
50 | uniform float glowPower;
51 |
52 |
53 | ${shader.replace(
54 | '#include ',
55 | /*glsl*/ `
56 | float rim = pow(1.0 - dot(normal, vec3(0, 0, 1)), glowPower);
57 |
58 | float a = min(1.0, rim * glowIntensity);
59 | outgoingLight = outgoingLight * (1.0 - a) + a * glowColor;
60 |
61 | #include
62 | `
63 | )}
64 | `;
65 |
66 | return shader;
67 | },
68 | };
69 |
70 | export interface MeshPhysicalMaterialWithGlowProps extends MeshPhysicalMaterialProps {
71 | glowIntensity: number;
72 | glowColor: Color;
73 | glowPower: number;
74 | }
75 |
76 | export const MeshPhysicalMaterialWithGlow = forwardRef((props, ref) => {
77 | const {
78 | glowColor,
79 | glowIntensity,
80 | glowPower,
81 | ...rest
82 | } = props;
83 | return ;
94 | });
95 |
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/components/GradientSphere/ReactExtendedMaterial.tsx:
--------------------------------------------------------------------------------
1 | // https://github.com/leoncvlt/three-extended-material
2 | /* eslint-disable */
3 | import { forwardRef, useEffect, useMemo, useRef } from "react";
4 | import { ExtendedMaterial as ImplExtendedMaterial } from "./ExtendedMaterial";
5 |
6 | const mapExtsNames = (array: any[]) => array.map(e => e.name || "unknown-extension").toString();
7 |
8 | export const ReactExtendedMaterial = forwardRef((props: any, ref) => {
9 | const { superMaterial, extensions, parameters, options, ...rest } = props;
10 | const implMaterialRef = useRef();
11 |
12 | const material = useMemo(() => {
13 | const recreate = () => {
14 | implMaterialRef.current = new (ImplExtendedMaterial as any)(superMaterial, extensions, parameters, options);
15 | implMaterialRef.current.__extensions = mapExtsNames(extensions);
16 | };
17 |
18 | const t = new superMaterial();
19 | if (!implMaterialRef.current
20 | || t.type !== implMaterialRef.current.type
21 | || mapExtsNames(extensions) !== implMaterialRef.current.__extensions) {
22 | recreate();
23 | }
24 | t.dispose();
25 | return implMaterialRef.current;
26 | }, [superMaterial, ...extensions, ...Object.values(parameters || {})]);
27 |
28 | useEffect(() => material.dispose(), [material]);
29 |
30 | return ;
31 | });
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/components/GradientSphere/assets/gradient1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/features/effects/components/SphereEffect/components/GradientSphere/assets/gradient1.jpg
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/components/GradientSphere/assets/gradient2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/features/effects/components/SphereEffect/components/GradientSphere/assets/gradient2.jpg
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/components/GradientSphere/assets/gradient3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fedgkr/feconf2024/e3d1fee15b66c8927cd4c61fdb747283e9cc0211/src/features/effects/components/SphereEffect/components/GradientSphere/assets/gradient3.jpg
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/components/GradientSphere/external/BlurShader.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/mrdoob/three.js/blob/dev/examples/jsm/shaders/HorizontalBlurShader.js
2 |
3 | export const BlurShader = {
4 | name: 'BlurShader',
5 | uniforms: {
6 | tDiffuse: { value: null },
7 | uBlurOffset: { value: 1 / 512 },
8 | },
9 | vertexShader: `
10 |
11 | varying vec2 vUv;
12 |
13 | void main() {
14 |
15 | vUv = uv;
16 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
17 |
18 | }`,
19 | fragmentShader: `
20 |
21 | uniform sampler2D tDiffuse;
22 | uniform float uBlurOffset;
23 |
24 | varying vec2 vUv;
25 |
26 | void main() {
27 | gl_FragColor = 0.25 * (
28 | texture2D( tDiffuse, vUv + uBlurOffset )
29 | + texture2D( tDiffuse, vUv - uBlurOffset )
30 | + texture2D( tDiffuse, vUv + uBlurOffset * vec2( 1., -1. ) )
31 | + texture2D( tDiffuse, vUv + uBlurOffset * vec2( -1., 1. ) )
32 | );
33 | }`,
34 | };
35 |
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/components/GradientSphere/external/BokehPass2.js:
--------------------------------------------------------------------------------
1 | import { BokehPass } from 'three/examples/jsm/Addons.js';
2 |
3 | export class BokehPass2 extends BokehPass {
4 | render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive*/) {
5 | // Render depth into texture
6 | this.scene.overrideMaterial = this.materialDepth;
7 |
8 | renderer.getClearColor(this._oldClearColor);
9 | const oldClearAlpha = renderer.getClearAlpha();
10 | const oldAutoClear = renderer.autoClear;
11 | renderer.autoClear = false;
12 |
13 | renderer.setClearColor(0xffffff, 0);
14 | renderer.setClearAlpha(0.0);
15 |
16 | renderer.setRenderTarget(this.renderTargetDepth);
17 | renderer.clear();
18 | renderer.render(this.scene, this.camera);
19 |
20 | // Render bokeh composite
21 |
22 | this.uniforms['tColor'].value = readBuffer.texture;
23 | this.uniforms['nearClip'].value = this.camera.near;
24 | this.uniforms['farClip'].value = this.camera.far;
25 |
26 | if (this.renderToScreen) {
27 | renderer.setRenderTarget(null);
28 | this.fsQuad.render(renderer);
29 | } else {
30 | renderer.setRenderTarget(writeBuffer);
31 | renderer.clear();
32 | this.fsQuad.render(renderer);
33 | }
34 | this.scene.overrideMaterial = null;
35 | renderer.setClearColor(this._oldClearColor);
36 | renderer.setClearAlpha(oldClearAlpha);
37 | renderer.autoClear = oldAutoClear;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/components/GradientSphere/external/BokehShader.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/mrdoob/three.js/blob/master/examples/jsm/shaders/BokehShader.js
2 |
3 | export const BokehShader = {
4 | name: 'BokehShader',
5 | uniforms: {
6 | tDiffuse: { value: null },
7 | focus: { value: 1.0 },
8 | aspect: { value: 1.0 },
9 | aperture: { value: 0.025 },
10 | maxblur: { value: 0.01 },
11 | nearClip: { value: 1.0 },
12 | farClip: { value: 1000.0 },
13 | },
14 | vertexShader: `
15 | varying vec2 vUv;
16 |
17 | void main() {
18 | vUv = uv;
19 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
20 | }`,
21 |
22 | fragmentShader: `
23 | varying vec2 vUv;
24 | uniform sampler2D tDiffuse;
25 |
26 | uniform float maxblur; // max blur amount
27 | // uniform float aperture; // aperture - bigger values for shallower depth of field
28 | // uniform float nearClip;
29 | // uniform float farClip;
30 |
31 | // uniform float focus;
32 |
33 |
34 | void main() {
35 | vec2 aspectcorrect = vec2( 1.0, 1.0 );
36 | vec2 dofblur = vec2 (maxblur, maxblur);
37 | vec2 dofblur9 = dofblur * 0.9;
38 | vec2 dofblur7 = dofblur * 0.7;
39 | vec2 dofblur4 = dofblur * 0.4;
40 |
41 | vec4 col = vec4( 0.0 );
42 |
43 | col += texture2D( tDiffuse, vUv.xy );
44 | // 16
45 | col += texture2D( tDiffuse, vUv.xy + vec2( 0.0, 0.4 ) * dofblur );
46 | col += texture2D( tDiffuse, vUv.xy + vec2( 0.15, 0.37 ) * dofblur );
47 | // col += texture2D( tDiffuse, vUv.xy + vec2( 0.29, 0.29 ) * dofblur );
48 | col += texture2D( tDiffuse, vUv.xy + vec2( -0.37, 0.15 ) * dofblur );
49 | // col += texture2D( tDiffuse, vUv.xy + vec2( 0.40, 0.0 ) * dofblur );
50 | col += texture2D( tDiffuse, vUv.xy + vec2( 0.37, -0.15 ) * dofblur );
51 | // col += texture2D( tDiffuse, vUv.xy + vec2( 0.29, -0.29 ) * dofblur );
52 | col += texture2D( tDiffuse, vUv.xy + vec2( -0.15, -0.37 ) * dofblur );
53 | col += texture2D( tDiffuse, vUv.xy + vec2( 0.0, -0.4 ) * dofblur );
54 | col += texture2D( tDiffuse, vUv.xy + vec2( -0.15, 0.37 ) * dofblur );
55 | // col += texture2D( tDiffuse, vUv.xy + vec2( -0.29, 0.29 ) * dofblur );
56 | col += texture2D( tDiffuse, vUv.xy + vec2( 0.37, 0.15 ) * dofblur );
57 | // col += texture2D( tDiffuse, vUv.xy + vec2( -0.4, 0.0 ) * dofblur );
58 | col += texture2D( tDiffuse, vUv.xy + vec2( -0.37, -0.15 ) * dofblur );
59 | // col += texture2D( tDiffuse, vUv.xy + vec2( -0.29, -0.29 ) * dofblur );
60 | col += texture2D( tDiffuse, vUv.xy + vec2( 0.15, -0.37 ) * dofblur );
61 |
62 | // 8
63 | // col += texture2D( tDiffuse, vUv.xy + vec2( 0.15, 0.37 ) * dofblur9 );
64 | col += texture2D( tDiffuse, vUv.xy + vec2( -0.37, 0.15 ) * dofblur9 );
65 | // col += texture2D( tDiffuse, vUv.xy + vec2( 0.37, -0.15 ) * dofblur9 );
66 | // col += texture2D( tDiffuse, vUv.xy + vec2( -0.15, -0.37 ) * dofblur9 );
67 | col += texture2D( tDiffuse, vUv.xy + vec2( -0.15, 0.37 ) * dofblur9 );
68 | // col += texture2D( tDiffuse, vUv.xy + vec2( 0.37, 0.15 ) * dofblur9 );
69 | col += texture2D( tDiffuse, vUv.xy + vec2( -0.37, -0.15 ) * dofblur9 );
70 | // col += texture2D( tDiffuse, vUv.xy + vec2( 0.15, -0.37 ) * dofblur9 );
71 |
72 | // 8
73 | // col += texture2D( tDiffuse, vUv.xy + vec2( 0.29, 0.29 ) * dofblur7 );
74 | // col += texture2D( tDiffuse, vUv.xy + vec2( 0.40, 0.0 ) * dofblur7 );
75 | // col += texture2D( tDiffuse, vUv.xy + vec2( 0.29, -0.29 ) * dofblur7 );
76 | // col += texture2D( tDiffuse, vUv.xy + vec2( 0.0, -0.4 ) * dofblur7 );
77 | col += texture2D( tDiffuse, vUv.xy + vec2( -0.29, 0.29 ) * dofblur7 );
78 | // col += texture2D( tDiffuse, vUv.xy + vec2( -0.4, 0.0 ) * dofblur7 );
79 | // col += texture2D( tDiffuse, vUv.xy + vec2( -0.29, -0.29 ) * dofblur7 );
80 | // col += texture2D( tDiffuse, vUv.xy + vec2( 0.0, 0.4 ) * dofblur7 );
81 |
82 | // 8
83 | // col += texture2D( tDiffuse, vUv.xy + vec2( 0.29, 0.29 ) * dofblur4 );
84 | // col += texture2D( tDiffuse, vUv.xy + vec2( 0.4, 0.0 ) * dofblur4 );
85 | // col += texture2D( tDiffuse, vUv.xy + vec2( 0.29, -0.29 ) * dofblur4 );
86 | // col += texture2D( tDiffuse, vUv.xy + vec2( 0.0, -0.4 ) * dofblur4 );
87 | // col += texture2D( tDiffuse, vUv.xy + vec2( -0.29, 0.29 ) * dofblur4 );
88 | // col += texture2D( tDiffuse, vUv.xy + vec2( -0.4, 0.0 ) * dofblur4 );
89 | // col += texture2D( tDiffuse, vUv.xy + vec2( -0.29, -0.29 ) * dofblur4 );
90 | // col += texture2D( tDiffuse, vUv.xy + vec2( 0.0, 0.4 ) * dofblur4 );
91 |
92 | gl_FragColor = col / 15.0;
93 | gl_FragColor.a = 1.0;
94 |
95 | }`,
96 | };
97 |
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/components/GradientSphere/external/FixedNoiseEffect.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/pmndrs/postprocessing/blob/main/src/effects/NoiseEffect.js
2 | import { Effect, BlendFunction } from 'postprocessing';
3 |
4 | const fragmentShader = `void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
5 | float time = 1.0;
6 | vec3 noise = vec3(rand(uv * (1.0 + time)));
7 |
8 | #ifdef PREMULTIPLY
9 |
10 | outputColor = vec4(min(inputColor.rgb * noise, vec3(1.0)), inputColor.a);
11 |
12 | #else
13 |
14 | outputColor = vec4(noise, inputColor.a);
15 |
16 | #endif
17 |
18 | }`;
19 |
20 | export default class FixedNoiseEffect extends Effect {
21 | /**
22 | * Constructs a new noise effect.
23 | *
24 | * @param {Object} [options] - The options.
25 | * @param {BlendFunction} [options.blendFunction=BlendFunction.SCREEN] - The blend function of this effect.
26 | * @param {Boolean} [options.premultiply=false] - Whether the noise should be multiplied with the input colors prior to blending.
27 | */
28 |
29 | constructor({
30 | blendFunction = BlendFunction.SCREEN,
31 | premultiply = false,
32 | } = {}) {
33 | super('FixedNoiseEffect', fragmentShader, { blendFunction });
34 | this.premultiply = premultiply;
35 | }
36 |
37 | /**
38 | * Indicates whether noise will be multiplied with the input colors prior to blending.
39 | *
40 | * @type {Boolean}
41 | */
42 |
43 | get premultiply() {
44 | return this.defines.has('PREMULTIPLY');
45 | }
46 |
47 | set premultiply(value) {
48 | if (this.premultiply !== value) {
49 | if (value) {
50 | this.defines.set('PREMULTIPLY', '1');
51 | } else {
52 | this.defines.delete('PREMULTIPLY');
53 | }
54 | this.setChanged();
55 | }
56 | }
57 |
58 | /**
59 | * Indicates whether noise will be multiplied with the input colors prior to blending.
60 | *
61 | * @deprecated Use premultiply instead.
62 | * @return {Boolean} Whether noise is premultiplied.
63 | */
64 |
65 | isPremultiplied() {
66 | return this.premultiply;
67 | }
68 |
69 | /**
70 | * Controls whether noise should be multiplied with the input colors prior to blending.
71 | *
72 | * @deprecated Use premultiply instead.
73 | * @param {Boolean} value - Whether noise should be premultiplied.
74 | */
75 |
76 | setPremultiplied(value: boolean) {
77 | this.premultiply = value;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/components/GradientSphere/external/KawaseBlurPass.js:
--------------------------------------------------------------------------------
1 | // https://github.com/ycw/three-kawase-blur
2 | import {
3 | EffectComposer,
4 | Pass,
5 | FullScreenQuad,
6 | } from 'three/examples/jsm/Addons.js';
7 | import { KawaseBlurShader } from './KawaseBlurShader.js';
8 | import {
9 | UniformsUtils,
10 | ShaderMaterial,
11 | WebGLRenderTarget,
12 | Vector2,
13 | } from 'three';
14 |
15 | class _KawaseBlurPass extends Pass {
16 | constructor(uOffset) {
17 | super();
18 |
19 | const uniforms = UniformsUtils.clone(KawaseBlurShader.uniforms);
20 | uniforms.uOffset.value = uOffset;
21 | // uniforms.uOpacity.value = uOpacity;
22 |
23 | const material = new ShaderMaterial({
24 | uniforms,
25 | vertexShader: KawaseBlurShader.vertexShader,
26 | fragmentShader: KawaseBlurShader.fragmentShader,
27 | });
28 |
29 | this.fsQuad = new FullScreenQuad(material);
30 | this.uniforms = material.uniforms;
31 | this.shouldRenderToSreen = false;
32 | }
33 |
34 | render(renderer, writeBuffer, readBuffer) {
35 | this.uniforms['tDiffuse'].value = readBuffer.texture;
36 |
37 | if (this.shouldRenderToSreen) {
38 | renderer.setRenderTarget(null);
39 | } else {
40 | renderer.setRenderTarget(writeBuffer);
41 | if (this.clear) renderer.clear();
42 | }
43 | this.fsQuad.render(renderer);
44 | }
45 | }
46 |
47 | export class KawaseBlurPass extends Pass {
48 | constructor({ renderer, blurCount, blurOffset, opacity = 1 }) {
49 | super();
50 | this._blurOpacity = opacity;
51 | this._renderer = renderer;
52 | this._internalComposer = new EffectComposer(
53 | renderer,
54 | new WebGLRenderTarget(0, 0)
55 | );
56 | this.setBlurs(blurOffset, blurCount);
57 | }
58 |
59 | getKernels() {
60 | return Array.from(this._kernels);
61 | }
62 |
63 | setBlurs(blurOffset, blurCount) {
64 | const kernels = [];
65 |
66 | for (let i = 0; i < blurCount; ++i) {
67 | kernels.push(blurOffset);
68 | }
69 | const res = this._renderer.getDrawingBufferSize(new Vector2());
70 |
71 | for (const [i, k] of kernels.entries()) {
72 | const uOffset = new Vector2().setScalar(0.5 + k).divide(res);
73 |
74 | const pass = this._internalComposer.passes[i];
75 | if (pass) {
76 | // reuse
77 | pass.uniforms.uOffset.value = uOffset;
78 |
79 | pass.shouldRenderToSreen = false;
80 | } else {
81 | this._internalComposer.addPass(new _KawaseBlurPass(uOffset, 1));
82 | }
83 | }
84 |
85 | this._internalComposer.passes.length = kernels.length; // rm unused
86 | this._internalComposer.reset();
87 | this._kernels = Array.from(kernels);
88 | this._blurOpacity = 1;
89 | this._blurOffset = blurOffset;
90 | this._blurCount = blurCount;
91 | }
92 |
93 | render(renderer, writeBuffer, readBuffer) {
94 | if (this._kernels.length === 0) return;
95 | // ---- transfer ownership
96 | this._internalComposer.readBuffer = readBuffer;
97 | this._internalComposer.writeBuffer = writeBuffer;
98 | this._internalComposer.passes[
99 | this._internalComposer.passes.length - 1
100 | ].shouldRenderToSreen = this.renderToScreen;
101 | this._internalComposer.render(renderer);
102 | }
103 |
104 | setSize(w, h) {
105 | this._internalComposer.setSize(w, h);
106 | this.setBlurs(this._blurOffset, this._blurOpacity);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/components/GradientSphere/external/KawaseBlurShader.js:
--------------------------------------------------------------------------------
1 | // https://github.com/ycw/three-kawase-blur
2 | export const KawaseBlurShader = {
3 | uniforms: {
4 | tDiffuse: { value: null },
5 | uOffset: { value: null },
6 | uOpacity: { value: null },
7 | },
8 | vertexShader: /* glsl */ `
9 | varying vec2 vUv;
10 | void main() {
11 | vUv = uv;
12 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
13 | }`,
14 | fragmentShader: /* glsl */ `
15 | uniform sampler2D tDiffuse;
16 | uniform vec2 uOffset;
17 | varying vec2 vUv;
18 | void main() {
19 | gl_FragColor = 0.25 * (
20 | texture2D( tDiffuse, vUv + uOffset )
21 | + texture2D( tDiffuse, vUv - uOffset )
22 | + texture2D( tDiffuse, vUv + uOffset * vec2( 1., -1. ) )
23 | + texture2D( tDiffuse, vUv + uOffset * vec2( -1., 1. ) )
24 | );
25 | // gl_FragColor.a *= 0.9;
26 | }
27 | `,
28 | };
29 |
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/components/GradientSphere/external/ReactFixedNoiseEffect.ts:
--------------------------------------------------------------------------------
1 | import { wrapEffect } from '@react-three/postprocessing';
2 | import { BlendFunction } from 'postprocessing';
3 | import FixedNoiseEffect from './FixedNoiseEffect';
4 |
5 | export const ReactFixedNoiseEffect = wrapEffect(FixedNoiseEffect, {
6 | blendFunction: BlendFunction.COLOR_DODGE,
7 | });
8 |
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/components/GradientSphere/external/_BlurPass.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | // https://github.com/ycw/three-kawase-blur
3 | // https://github.com/mrdoob/three.js/blob/dev/examples/jsm/shaders/HorizontalBlurShader.js
4 |
5 | import {
6 | IUniform,
7 | ShaderMaterial,
8 | Vector2,
9 | WebGLRenderTarget,
10 | WebGLRenderer,
11 | } from 'three';
12 | import {
13 | EffectComposer,
14 | Pass,
15 | FullScreenQuad,
16 | } from 'three/examples/jsm/Addons.js';
17 |
18 | const BlurShader = {
19 | name: 'BlurShader',
20 |
21 | uniforms: {
22 | tDiffuse: { value: null },
23 | uBlurs: { value: [] },
24 | uCount: { value: 0 },
25 | uBlurCount: { value: 0 },
26 | uBlurOffset: { value: 0 },
27 | },
28 |
29 | vertexShader: /* glsl */ `
30 |
31 | varying vec2 vUv;
32 |
33 | void main() {
34 |
35 | vUv = uv;
36 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
37 |
38 | }`,
39 |
40 | fragmentShader: /* glsl */ `
41 |
42 | uniform sampler2D tDiffuse;
43 | uniform float uBlurs[200];
44 | uniform float uBlurCount;
45 | uniform float uBlurOffset;
46 | uniform int uCount;
47 | varying vec2 vUv;
48 |
49 | void main() {
50 | vec4 sum = vec4(0.0);
51 |
52 | // for (int i = 0; i < uCount; ++i) {
53 | // float x = float(uBlurs[i * 3 + 0]);
54 | // float y = float(uBlurs[i * 3 + 1]);
55 | // float mul = float(uBlurs[i * 3 + 2]);
56 |
57 | // vec2 pos = vUv + vec2(x, y);
58 |
59 | // if (pos.x <= 0.0 || pos.y <= 0.0 || pos.x >= 1.0 || pos.y >= 1.0) {
60 | // continue;
61 | // }
62 | // sum += mul * texture2D(tDiffuse, pos);
63 | // }
64 |
65 | gl_FragColor = 0.25 * (
66 | texture2D(tDiffuse, vUv + uBlurOffset)
67 | + texture2D(tDiffuse, vUv - uBlurOffset)
68 | + texture2D(tDiffuse, vUv + uBlurOffset * vec2(1.0, -1.0))
69 | + texture2D(tDiffuse, vUv + uBlurOffset * vec2(-1.0, 1.0))
70 | );
71 |
72 | // sum += texture2D( tDiffuse, vec2( vUv.x - 4.0 * uBlurOffset, vUv.y ) ) * 0.051;
73 | // sum += texture2D( tDiffuse, vec2( vUv.x - 3.0 * uBlurOffset, vUv.y ) ) * 0.0918;
74 | // sum += texture2D( tDiffuse, vec2( vUv.x - 2.0 * uBlurOffset, vUv.y ) ) * 0.12245;
75 | // sum += texture2D( tDiffuse, vec2( vUv.x - 1.0 * uBlurOffset, vUv.y ) ) * 0.1531;
76 | // sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y ) ) * 0.1633;
77 | // sum += texture2D( tDiffuse, vec2( vUv.x + 1.0 * uBlurOffset, vUv.y ) ) * 0.1531;
78 | // sum += texture2D( tDiffuse, vec2( vUv.x + 2.0 * uBlurOffset, vUv.y ) ) * 0.12245;
79 | // sum += texture2D( tDiffuse, vec2( vUv.x + 3.0 * uBlurOffset, vUv.y ) ) * 0.0918;
80 | // sum += texture2D( tDiffuse, vec2( vUv.x + 4.0 * uBlurOffset, vUv.y ) ) * 0.051;
81 |
82 | gl_FragColor = sum;
83 | }`,
84 | };
85 |
86 | function fact(main: number) {
87 | let j = 1;
88 |
89 | for (let i = 1; i <= main; ++i) {
90 | j *= i;
91 | }
92 | return j;
93 | }
94 |
95 | export class InternalBlurPass extends Pass {
96 | fsQuad: FullScreenQuad;
97 | uniforms: Record;
98 | material: ShaderMaterial;
99 | constructor(public renderer: WebGLRenderer) {
100 | super();
101 |
102 | // const uniforms = UniformsUtils.clone(BlurShader.uniforms);
103 | this.material = new ShaderMaterial(BlurShader);
104 | this.fsQuad = new FullScreenQuad(this.material);
105 | this.uniforms = this.material.uniforms;
106 | }
107 |
108 | setBlurs(blurOffset: number, blurCount: number) {
109 | const res = this.renderer.getDrawingBufferSize(new Vector2());
110 |
111 | function getBlurOffsets(count: number) {
112 | const obj: Record = {};
113 |
114 | for (let x = -count; x <= count; ++x) {
115 | for (let y = -count; y <= count; ++y) {
116 | const absX = Math.abs(x);
117 | const absY = Math.abs(y);
118 | const isOddX = !!(absX % 2);
119 | const isOddY = !!(absY % 2);
120 |
121 | if (absX + absY > count) {
122 | continue;
123 | }
124 |
125 | for (let num = 1; num <= count; ++num) {
126 | const isOddNum = !!(num % 2);
127 |
128 | if (
129 | (isOddNum && !isOddX && !isOddY) ||
130 | (!isOddNum && isOddX !== isOddY) ||
131 | absX > num ||
132 | absY > num ||
133 | absX + absY > num
134 | ) {
135 | continue;
136 | }
137 | for (let xCount = isOddX ? 1 : 0; xCount <= num; xCount += 2) {
138 | const yCount = num - xCount;
139 |
140 | if (absX > xCount || absY > yCount || isOddY !== !!(yCount % 2)) {
141 | continue;
142 | }
143 |
144 | const mainX = (absX + xCount) / 2;
145 | const mainY = (absY + yCount) / 2;
146 |
147 | const caseCount =
148 | fact(xCount + yCount) /
149 | fact(xCount - mainX) /
150 | fact(mainX) /
151 | fact(yCount - mainY) /
152 | fact(mainY);
153 |
154 | obj[`${x}x${y}`] = (obj[`${x}x${y}`] || 0) + caseCount;
155 | }
156 | }
157 | }
158 | }
159 | return obj;
160 | }
161 |
162 | const offsets = getBlurOffsets(blurCount - 1);
163 | const blurs: number[] = [];
164 | const entries = Object.entries(offsets);
165 | const mul = Math.pow(0.2, blurCount);
166 | const zeroVec = new Vector2(0, 0);
167 | const posMul = Math.pow(2, 0.5);
168 |
169 | entries.forEach(([k, v]) => {
170 | let pos = k.split('x').map(v => parseFloat(v));
171 | const opacity = v * mul;
172 |
173 | if (opacity < 0.001) {
174 | return;
175 | }
176 | const vec = new Vector2(pos[0], pos[1]);
177 | const vec2 = vec.rotateAround(zeroVec, Math.PI / 4);
178 |
179 | pos = [posMul * vec2.x, posMul * vec2.y];
180 | blurs.push(
181 | (blurOffset * pos[0]) / res.x,
182 | (blurOffset * pos[1]) / res.y,
183 | opacity
184 | );
185 | });
186 | // console.log(offsets, blurs);s
187 |
188 | // console.log(true, getBlurOffsets([0, 0], arr.length));
189 | // console.log(false, getBlurOffsets2(arr.length));
190 | // uBlurOffset
191 | this.uniforms.uBlurOffset.value = blurOffset / res.y;
192 | this.uniforms.uBlurs.value = blurs;
193 | this.uniforms.uBlurCount.value = blurCount - 1;
194 | this.uniforms.uCount.value = entries.length;
195 | }
196 | render(
197 | renderer: WebGLRenderer,
198 | writeBuffer: WebGLRenderTarget,
199 | readBuffer: WebGLRenderTarget
200 | ) {
201 | this.uniforms['tDiffuse'].value = readBuffer.texture;
202 | this.material.needsUpdate = true;
203 |
204 | if (this.renderToScreen) {
205 | renderer.setRenderTarget(null);
206 | } else {
207 | renderer.setRenderTarget(writeBuffer);
208 | if (this.clear) renderer.clear();
209 | }
210 | this.fsQuad.render(renderer);
211 | }
212 | dispose() {
213 | this.material.dispose();
214 | this.fsQuad.dispose();
215 | }
216 | }
217 |
218 | export class BlurPass extends Pass {
219 | _internalComposer: EffectComposer;
220 | _pass: InternalBlurPass;
221 | _blurOffset = 0;
222 | _blurCount = 0;
223 | constructor(public renderer: WebGLRenderer) {
224 | super();
225 | this.renderer = renderer;
226 | this._internalComposer = new EffectComposer(
227 | renderer,
228 | new WebGLRenderTarget(0, 0)
229 | );
230 |
231 | this._pass = new InternalBlurPass(renderer);
232 | this._internalComposer.addPass(this._pass);
233 | }
234 |
235 | setBlurs(blurOffset: number, blurCount: number) {
236 | this._blurCount = blurCount;
237 | this._blurOffset = blurOffset;
238 | this._pass.setBlurs(blurOffset, blurCount);
239 | this._internalComposer.reset();
240 | }
241 | render(
242 | renderer: WebGLRenderer,
243 | writeBuffer: WebGLRenderTarget,
244 | readBuffer: WebGLRenderTarget
245 | ) {
246 | this._internalComposer.readBuffer = readBuffer;
247 | this._internalComposer.writeBuffer = writeBuffer;
248 | // @ts-ignore
249 | this._internalComposer.render(renderer);
250 | }
251 |
252 | setSize(w: number, h: number) {
253 | this._internalComposer.setSize(w, h);
254 | this.setBlurs(this._blurOffset, this._blurCount);
255 | }
256 | }
257 |
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/components/GradientSphere/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './GradientSphere';
2 |
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/components/index.ts:
--------------------------------------------------------------------------------
1 | // export { default as Background } from './Background';
2 | export { default as GradientSphere } from './GradientSphere';
3 |
--------------------------------------------------------------------------------
/src/features/effects/components/SphereEffect/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './SphereEffect';
2 |
--------------------------------------------------------------------------------
/src/features/effects/components/index.ts:
--------------------------------------------------------------------------------
1 | export { AuroraContainer } from './AuroraContainer';
2 | export { SphereContainer } from './SphereContainer';
3 | export { default as SphereEffect } from './SphereEffect';
4 |
--------------------------------------------------------------------------------
/src/features/hooks/useHeroScreen.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useMemo, useRef } from 'react';
2 |
3 | export function useHeroScreen() {
4 | const sizeRef = useRef({ width: 0, height: 0 });
5 | const scrollRef = useRef(0);
6 | const screenSizeRef = useRef([]);
7 | const target = useMemo(() => new EventTarget(), []);
8 |
9 | const scroll = useCallback(
10 | (unScroll?: boolean) => {
11 | scrollRef.current = document.documentElement.scrollTop;
12 | if (!unScroll) target.dispatchEvent(new CustomEvent('scroll'));
13 | },
14 | [target]
15 | );
16 | const resize = useCallback(
17 | (unDispatch?: boolean) => {
18 | const width = window.innerWidth;
19 | const height = window.innerHeight;
20 |
21 | sizeRef.current = { width, height };
22 | screenSizeRef.current = [...document.querySelectorAll('section')].map(
23 | el => {
24 | return el.getBoundingClientRect();
25 | }
26 | );
27 | if (!unDispatch) {
28 | target.dispatchEvent(new CustomEvent('resize'));
29 | }
30 | },
31 | [target]
32 | );
33 |
34 | const onScroll = useCallback(
35 | (callback: () => void) => {
36 | target.addEventListener('scroll', callback);
37 | },
38 | [target]
39 | );
40 |
41 | const onResize = useCallback(
42 | (callback: () => void) => {
43 | target.addEventListener('resize', callback);
44 | },
45 | [target]
46 | );
47 |
48 | useEffect(() => {
49 | const resizeCallback = () => resize();
50 | const scrollCallback = () => scroll();
51 |
52 | window.addEventListener('resize', resizeCallback);
53 | window.addEventListener('scroll', scrollCallback);
54 |
55 | resizeCallback();
56 | scrollCallback();
57 | return () => {
58 | window.removeEventListener('resize', resizeCallback);
59 | window.removeEventListener('scroll', scrollCallback);
60 | };
61 | }, [resize, scroll]);
62 |
63 | return {
64 | sizeRef,
65 | scrollRef,
66 | screenSizeRef,
67 | scroll,
68 | resize,
69 | onScroll,
70 | onResize,
71 | dispatch(eventName: string) {
72 | target.dispatchEvent(new CustomEvent(eventName));
73 | },
74 | };
75 | }
76 |
--------------------------------------------------------------------------------
/src/features/hooks/usePrefersReducedMotion.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | /* eslint-disable react-hooks/exhaustive-deps */
3 | /**
4 | * Reference: https://www.joshwcomeau.com/react/prefers-reduced-motion/
5 | */
6 | import { useCallback, useEffect, useRef } from 'react';
7 |
8 | const QUERY = '(prefers-reduced-motion: no-preference)';
9 |
10 | export function usePrefersReducedMotionRef(
11 | _callback: (isReduced: boolean) => void
12 | ) {
13 | const callback = useCallback(_callback, []);
14 | const reducedMotionRef = useRef(false);
15 |
16 | useEffect(() => {
17 | const mediaQueryList = window.matchMedia(QUERY);
18 | const listener = (event: any) => {
19 | const isReduced = !event.matches;
20 | reducedMotionRef.current = isReduced;
21 | callback(isReduced);
22 | };
23 |
24 | listener(window.matchMedia(QUERY));
25 | mediaQueryList.addEventListener('change', listener);
26 | return () => {
27 | mediaQueryList.removeEventListener('change', listener);
28 | };
29 | }, []);
30 |
31 | return reducedMotionRef;
32 | }
33 |
--------------------------------------------------------------------------------
/src/features/programs/constants/index.ts:
--------------------------------------------------------------------------------
1 | const sessionTimeLabelLookup: Record = {
2 | 1: '12:50 - 13:00',
3 | 2: '13:00 - 13:40',
4 | 3: '14:00 - 14:40',
5 | 4: '15:00 - 15:40',
6 | 5: '16:00 - 16:40',
7 | 6: '17:00 - 17:40',
8 | };
9 |
10 | const lightningSessionTimeLabelLookup: Record = {
11 | 1: '13:00 - 13:40',
12 | 2: '13:45 - 14:25',
13 | 3: '14:35 - 15:15',
14 | 4: '15:25 - 16:05',
15 | 5: '16:15 - 16:55',
16 | 6: '17:00 - 17:40',
17 | };
18 |
19 | export { sessionTimeLabelLookup, lightningSessionTimeLabelLookup };
20 |
--------------------------------------------------------------------------------
/src/features/programs/contexts/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import {
4 | createContext,
5 | FC,
6 | PropsWithChildren,
7 | useContext,
8 | useState,
9 | } from 'react';
10 | import { SessionType } from '~/features/programs/types';
11 | import { noop } from 'lodash-es';
12 |
13 | interface ProgramContextType {
14 | currentTab: SessionType;
15 | onChangeTab: (tab: SessionType) => void;
16 | }
17 |
18 | const ProgramContext = createContext({
19 | currentTab: SessionType.A,
20 | onChangeTab: noop,
21 | });
22 |
23 | const ProgramContextProvider: FC = ({ children }) => {
24 | const [currentTab, setCurrentTab] = useState(SessionType.A);
25 | return (
26 |
27 | {children}
28 |
29 | );
30 | };
31 |
32 | const useProgram = () => useContext(ProgramContext);
33 |
34 | export { ProgramContextProvider, useProgram };
35 |
--------------------------------------------------------------------------------
/src/features/programs/data/sessions.ts:
--------------------------------------------------------------------------------
1 | import { Session, SessionType } from '~/features/programs/types';
2 | import { filter } from 'lodash-es';
3 |
4 | const sessions: Session[] = [
5 | {
6 | type: SessionType.A,
7 | title: '후원세션',
8 | speakers: [],
9 | order: 1,
10 | },
11 | {
12 | type: SessionType.A,
13 | title: '에어브릿지 SDK팀이 Unit Testable한 코드를 작성하는 방법',
14 | description:
15 | '에어브릿지 SDK 팀에서 사용하는 플랫폼 의존적인 API를 Pure하게 감싸고, 이것을 주입하고, Mocking하고, 유닛테스트를 작성하는 방법에 대해서 소개합니다. 이는 코드를 더 이식하기 쉽게 만들어주고, 테스트의 속도를 높여주고, 코드가 자연스럽게 모듈화 되도록 유도합니다.',
16 | speakers: [
17 | {
18 | name: '최수범',
19 | company: 'AB180',
20 | },
21 | ],
22 | order: 2,
23 | },
24 | {
25 | type: SessionType.A,
26 | title: '브라우저를 여행하는 힙스터 개발자를 위한 웹 어셈블리 안내서',
27 | description:
28 | '웹 어셈블리의 탄생과 역사를 소개하고 브라우저의 플랫폼화로 인한 웹 어셈블리의 필요성을 예시를 언급하며 이야기 합니다.',
29 | speakers: [
30 | {
31 | name: '김강현',
32 | company: '광주 소프트웨어 마이스터고등학교',
33 | },
34 | ],
35 | order: 3,
36 | },
37 | {
38 | type: SessionType.A,
39 | title: '쉽고 편리한 E2E 테스트 자동화를 꿈꾸며...',
40 | description:
41 | '"서비스 운영에서 TDD만으로는 부족하다"
' +
42 | '백엔드도 TDD, 프론트엔드도 TDD로 부분 부분 테스트 코드를 통해 문제 없는 코드를 작성하지만, 실제 고객에게 보여지는 화면을 검증하는 것은 E2E 테스트로만 가능한 상황입니다. 지금은 사라진 Explore 시절부터 E2E 테스트를 시도해보며 겪었던 경험과 현재 트렌드에 가장 적합한 방법까지, 테스트를 보다 편리하게 자동화하기 위해 노력했던 경험을 공유합니다.',
43 | speakers: [
44 | {
45 | name: '백부석 (Kianue)',
46 | company: '스티비',
47 | },
48 | ],
49 | order: 4,
50 | },
51 | {
52 | type: SessionType.A,
53 | title: 'React Native와 웹이 공존하는 또 하나의 방법',
54 | description:
55 | 'Type Safe에 대한 이야기를 간략하게 합니다. React Native와 WebView를 활용해 개발을 할 때 마주칠 수 밖에 없는 통신 문제를 Type Safe하고, 흔하지 않게 해결하는 방법을 다룹니다. 또한 라이브러리를 개발하면서 어떻게 DX를 생각하며 개발할 수 있는지 다룹니다.',
56 | speakers: [
57 | {
58 | name: '강선규',
59 | company: '브랜더진',
60 | },
61 | ],
62 | order: 5,
63 | },
64 | {
65 | type: SessionType.A,
66 | title:
67 | '아�니 이 글자 왜 들어간거예요? (부제: 알아두면 가끔 쓸모있는 한글 유니코드 완전 정복)',
68 | description:
69 | '맥에서 첨부한 파일들 윈도우에서 파일 깨지는 경험 해본 적 있으신가요?
' +
70 | 'url 에 한글 넣고나면 한글이 아니라 인코딩으로 어떻게 변하는지 궁금한적 있으신가요?
' +
71 | '레거시 코드를 만져야 해서 euc-kr로 설정된 웹 페이지를 처리한 경험 있으신가요?
' +
72 | '한글 인코딩, 코드 이 세션으로 완전 정복 해드립니다.',
73 | speakers: [
74 | {
75 | name: '제한재',
76 | company: '데니어',
77 | },
78 | ],
79 | order: 6,
80 | },
81 | {
82 | type: SessionType.B,
83 | title: '후원세션',
84 | speakers: [],
85 | order: 1,
86 | },
87 | {
88 | type: SessionType.B,
89 | title: '바퀴 대신 로켓 만들기',
90 | description:
91 | '어드민 서비스에 매일 새로운 화면이 추가되고, 여러 개발자들이 각기 다른 방식으로 개발함에 따라 코드의 복잡성이 나날이 증가하는 상황이 남 일 같지 않다면, 기존의 개발 방식을 탈피하고 우리가 효율적이고 지속 가능한 방식으로 일할 수 있도록 도와줄 도구, 즉 우리만의 프레임워크가 필요한 시점일 수 있습니다.
' +
92 | '토스페이먼츠 팀에서 올해 상반기에 400개가 넘는 레거시 어드민 페이지를 이관하며 프레임워크를 통해 생산성을 유지하고 목표를 달성한 경험을 공유하고자 합니다.',
93 | speakers: [
94 | {
95 | name: '양의현',
96 | company: '토스페이먼츠',
97 | },
98 | ],
99 | order: 2,
100 | },
101 | {
102 | type: SessionType.B,
103 | title:
104 | '10만 글로벌 유저들이 생겨버린 Three.js 사이드 프로젝트 ─ ShaderGradient 개발기 (feat. 프레이머)',
105 | description: `다들 사이드 프로젝트 하시죠? 저도 해봤습니다..(일주일에 세 시간씩 친구 디자이너와)
106 | 근데 웬걸 반응이.. 심상치 않아서 최근 12만 유저를 찍고 돈도 벌어주는 (1년에 200만 원 정도) 프로젝트가 되어버렸다면 믿으시겠습니까? 어떻게 이런 결과물이 나올 수 있던 것일까요?
107 | 하고 싶은 거 다 해보고 (Three.js, GLSL, ESM, Store First Component Design, URL DB, monorepo, Web Application with Framer), 변화무쌍한 플랫폼에 적응도 해본 1년 반간의 개발 스토리와, 이 프로덕트를 사람들이 좋아해 줄 수 있었던 썰을 풀어보려고 합니다`,
108 | speakers: [
109 | {
110 | name: '지용민',
111 | company: '하버스쿨',
112 | },
113 | ],
114 | order: 3,
115 | },
116 | {
117 | type: SessionType.B,
118 | title: '모던 웹 기술로 C++ 기반 렌더링 엔진 테스트 자동화하기',
119 | description:
120 | '크로스 플랫폼을 지원하는 벡터 그래픽스 엔진에 대한 품질 검사를 Front-End 기술만으로 검증하도록 파이프라인을 개선한 경험에 대해 이야기합니다.
' +
121 | 'Windows, MacOS, Android, iOS 그리고 Linux에 이르기까지 모든 환경에서의 적합성 테스트를 웹 페이지에서 한 번에 처리하며, 수만건의 테스트를 자동화하였습니다.\n' +
122 | '모던 FE 기술이 얼마나 효과적으로 크로스 플랫폼 테스팅을 자동화할 수 있었는지, 모든 과정을 공유합니다.',
123 | speakers: [
124 | {
125 | name: '유진의',
126 | company: 'LottieFiles',
127 | },
128 | ],
129 | order: 4,
130 | },
131 | {
132 | type: SessionType.B,
133 | title: '메타버스 서비스(ZEP)에서 게임과 WebRTC를 함께 적용하기',
134 | description: `0. WebRTC, ZEP 소개
135 | 1. WebRTC 솔루션을 교체하며
136 | 2. React로 이동하며
137 | 3. 리팩터링을 해보며
138 | 4. 다양한 문제점과 해결`,
139 | speakers: [
140 | {
141 | name: '박영진',
142 | company: '네이버 with ZEP',
143 | },
144 | ],
145 | order: 5,
146 | },
147 | {
148 | type: SessionType.B,
149 | title: '7가지 플랫폼 서버로 프론트엔드 버프 마법 걸기',
150 | description:
151 | '프론트엔드를 강화하는 플랫폼 서버 7가지 사례를 공개합니다! 토스코어 프론트엔드 챕터에서 운영하는 다양한 서버들을 통해 폴리필, 이미지 최적화, OpenGraph 등 여러 문제를 해결하고 더 나은 UX/DX를 실현하는 방법을 소개합니다. 현업에서 즉시 활용 가능한 구체적인 사례와 적용 방법을 통해 프론트엔드를 강화하는 비법을 얻어가세요.',
152 | speakers: [
153 | {
154 | name: '정석호',
155 | company: '비바리퍼블리카',
156 | },
157 | ],
158 | order: 6,
159 | },
160 | {
161 | type: SessionType.Lightning,
162 | title: 'Group 1 : Motion',
163 | description:
164 | '정미량 - 프론트엔드 주도로 커머스 서비스 개발하기: 효율적 도구 활용
' +
165 | '유승완 - 개발자로서의 성장: 교육 프로그램과 함께한 2년의 여정
' +
166 | '김민수 - 성장하기 위한 모든 방법을 동원한다
' +
167 | '박정욱 - 개발자에게 일을 잘한다는 건 뭔가요?',
168 | speakers: [
169 | {
170 | name: '정미량',
171 | },
172 | {
173 | name: '유승완',
174 | },
175 | {
176 | name: '김민수',
177 | },
178 | {
179 | name: '박정욱',
180 | },
181 | ],
182 | order: 1,
183 | },
184 | {
185 | type: SessionType.Lightning,
186 | title: 'Group 2 : Collaborations',
187 | description:
188 | '박태호 - FE의 문화유산답사기 - 레거시 시스템 운영에 대하여
' +
189 | '배휘동 - AI 시대의 협업 전략: 3자간(AI + 코드 + 인간) 협업을 통한 대용량 JS 파일 리팩토링 경험
' +
190 | '최지민 - 프론트에서 백엔드로 전향한 후에 비로소 보이는 것들',
191 | speakers: [
192 | {
193 | name: '박태호',
194 | },
195 | {
196 | name: '배휘동',
197 | },
198 | {
199 | name: '최지민',
200 | },
201 | ],
202 | order: 2,
203 | },
204 | {
205 | type: SessionType.Lightning,
206 | title: 'Group 3 : Effective Engineer',
207 | description:
208 | '조성진 - 저는 vim을 쓰는 프론트엔드 개발자입니다.
' +
209 | '김무훈 - 프린트 레이아웃과 함께 웹 디자인하기 — CSS Paged Media
' +
210 | '박영진 - 입사한지 3개월 된 신입 개발자가 팀 생산성에 기여한 이야기 (feat. VSCode Extension)
' +
211 | '이재호 - 잘못된 리액트 훅의 사용을 미리 찾아보자: 리액트 훅의 엄밀한 의미구조 정의하기',
212 | speakers: [
213 | {
214 | name: '조성진',
215 | },
216 | {
217 | name: '김무훈',
218 | },
219 | {
220 | name: '박영진',
221 | },
222 | {
223 | name: '이재호',
224 | },
225 | ],
226 | order: 3,
227 | },
228 | {
229 | type: SessionType.Lightning,
230 | title: 'Group 4 : Your near',
231 | description:
232 | '홍서희 - 내가 어쩌다 리더가 됐지? : 신입 개발자의 우당탕탕 어드민 개발기
' +
233 | '최관수 - 사내 첫 리액트 주니어 개발자의 생존기
' +
234 | '강인영 - 디자이너와 일주일마다 햄버거 메뉴 고르기
' +
235 | '조영록 - 인터렉션을 단계별로 추상화하여 웹 페이지 개편하기',
236 | speakers: [
237 | {
238 | name: '홍서희',
239 | },
240 | {
241 | name: '최관수',
242 | },
243 | {
244 | name: '강인영',
245 | },
246 | {
247 | name: '조영록',
248 | },
249 | ],
250 | order: 4,
251 | },
252 | {
253 | type: SessionType.Lightning,
254 | title: 'Group 5 : 저는 말이조',
255 | description:
256 | '권경민 - 고졸 개발자로 살아남기
' +
257 | '김승모 - 우리, 같이 성장할래요?(부제: 커뮤니티와 함께 성장하기)
' +
258 | '임경희 - 사이드프로젝트로 웹 접근성 시작하기
' +
259 | '한재현 - 개발자라면 한 번쯤 궁금했을지도 모를 프로그래밍 강사 101',
260 | speakers: [
261 | {
262 | name: '권경민',
263 | },
264 | {
265 | name: '김승모',
266 | },
267 | {
268 | name: '임경희',
269 | },
270 | {
271 | name: '한재현',
272 | },
273 | ],
274 | order: 5,
275 | },
276 | {
277 | type: SessionType.Lightning,
278 | title: 'Group 6 : Explorers',
279 | description:
280 | '김관식 - 오픈소스 기여, 어렵지 않아요!
' +
281 | '박준영 - PVI - 나에게 잘 맞는 아키텍쳐를 찾아서
' +
282 | '신다혜 - <Tab />의 반전: 클릭 한 번으로 본 UI/UX와 비즈니스의 충돌
' +
283 | '김지연 - 3인 애자일 조직에서 프로덕트를 출시하기까지의 경험',
284 | speakers: [
285 | {
286 | name: '김관식',
287 | },
288 | {
289 | name: '박준영',
290 | },
291 | {
292 | name: '신다혜',
293 | },
294 | {
295 | name: '김지연',
296 | },
297 | ],
298 | order: 6,
299 | },
300 | ];
301 |
302 | const aSessionList = filter(sessions, { type: SessionType.A });
303 |
304 | const bSessionList = filter(sessions, { type: SessionType.B });
305 |
306 | const lightningSessionList = filter(sessions, { type: SessionType.Lightning });
307 |
308 | export { sessions, aSessionList, bSessionList, lightningSessionList };
309 |
--------------------------------------------------------------------------------
/src/features/programs/types/index.ts:
--------------------------------------------------------------------------------
1 | enum SessionType {
2 | A,
3 | B,
4 | Lightning,
5 | }
6 |
7 | interface Speaker {
8 | name: string;
9 | company?: string;
10 | }
11 |
12 | interface Session {
13 | type: SessionType;
14 | title: string;
15 | description?: string;
16 | speakers: Speaker[];
17 | order: number;
18 | }
19 |
20 | export { SessionType };
21 | export type { Session };
22 |
--------------------------------------------------------------------------------
/src/shared/components/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from '@styled-system/jsx';
2 |
3 | const Button = styled('a', {
4 | base: {
5 | display: 'inline-flex',
6 | flexDirection: 'column',
7 | alignItems: 'center',
8 | justifyContent: 'center',
9 | gap: '2px',
10 | fontWeight: '700',
11 | cursor: 'pointer',
12 | '&:not([href])': {
13 | cursor: 'not-allowed',
14 | },
15 | },
16 | variants: {
17 | size: {
18 | s: {
19 | width: '118px',
20 | height: '40px',
21 | fontSize: '14px',
22 | borderRadius: '6px',
23 | },
24 | m: {
25 | fontSize: {
26 | base: '16px',
27 | xl: '20px',
28 | },
29 | borderRadius: '10px',
30 | maxWidth: '300px',
31 | width: {
32 | base: '100%',
33 | xl: '240px',
34 | },
35 | height: {
36 | base: '53px',
37 | xl: '61px',
38 | },
39 | },
40 | },
41 | status: {
42 | active: {
43 | backgroundColor: '#FFFFFF',
44 | color: '#000000',
45 | },
46 | inactive: {
47 | backgroundColor: 'rgba(0, 0, 0, 0.3)',
48 | color: '#FFFFFF',
49 | border: '1px solid rgba(255, 255, 255, 0.2)',
50 | },
51 | },
52 | },
53 | });
54 |
55 | export default Button;
56 |
--------------------------------------------------------------------------------
/src/shared/components/Button/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Button';
2 |
--------------------------------------------------------------------------------
/src/shared/components/Column.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from '@styled-system/jsx';
2 |
3 | const Column = styled('div', {
4 | base: {
5 | display: 'flex',
6 | flexDirection: 'column',
7 | alignItems: 'center',
8 | },
9 | });
10 |
11 | export default Column;
12 |
--------------------------------------------------------------------------------
/src/shared/components/FadeIn.tsx:
--------------------------------------------------------------------------------
1 | import { FC, MouseEventHandler, PropsWithChildren, useMemo } from 'react';
2 | import { motion, Variants } from 'framer-motion';
3 | import { styled } from '@styled-system/jsx';
4 |
5 | interface Props {
6 | className?: string;
7 | distance?: 0 | 20 | 30 | 60 | 80;
8 | duration?: {
9 | in?: number;
10 | out?: number;
11 | };
12 | onClick?: MouseEventHandler;
13 | }
14 |
15 | const FadeIn: FC> = ({
16 | children,
17 | distance = 20,
18 | duration,
19 | ...props
20 | }) => {
21 | const variants: Variants = useMemo(
22 | () => ({
23 | visible: {
24 | opacity: 1,
25 | y: 0,
26 | transition: { duration: duration?.in ?? 0.4 },
27 | },
28 | hidden: {
29 | opacity: 0,
30 | y: `${distance}px`,
31 | transition: { duration: duration?.out ?? 0.25 },
32 | },
33 | }),
34 | [distance, duration]
35 | );
36 | return (
37 |
38 | {children}
39 |
40 | );
41 | };
42 |
43 | const Container = styled(motion.div, {
44 | base: {
45 | opacity: 0,
46 | },
47 | variants: {
48 | y: {
49 | 0: {
50 | transform: 'translateY(0)',
51 | },
52 | 20: {
53 | transform: 'translateY(20px)',
54 | },
55 | 30: {
56 | transform: 'translateY(30px)',
57 | },
58 | 60: {
59 | transform: 'translateY(60px)',
60 | },
61 | 80: {
62 | transform: 'translateY(80px)',
63 | },
64 | },
65 | },
66 | });
67 |
68 | export default FadeIn;
69 |
--------------------------------------------------------------------------------
/src/shared/components/Footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import { styled } from '@styled-system/jsx';
3 |
4 | import { Logo } from './components';
5 | import { MailIcon, GlobalIcon, FAQIcon, FacebookIcon } from './icons';
6 | import { EMAIL, FAQ_LINK, FE_GROUP_LINK } from '~/shared/constants';
7 |
8 | const Footer: FC = () => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | feconf@googlegroups.com
21 |
22 |
23 |
24 |
25 |
26 | 프론트엔드개발그룹
27 |
28 |
29 |
30 |
31 |
32 | FEConf 2023
33 |
34 |
35 |
36 |
37 |
38 | FAQ
39 |
40 |
41 | © FEConf 2024 All rights reserved.
42 |
43 |
44 | );
45 | };
46 |
47 | const Container = styled('footer', {
48 | base: {},
49 | });
50 |
51 | const Wrap = styled('div', {
52 | base: {
53 | position: 'relative',
54 | display: 'flex',
55 | flexDirection: {
56 | base: 'column',
57 | xl: 'row',
58 | },
59 | justifyContent: 'space-between',
60 | padding: {
61 | base: '20px 16px 60px 16px',
62 | xl: '64px 80px 60px 80px',
63 | },
64 | gap: {
65 | base: '22px',
66 | xl: 'initial',
67 | },
68 | maxWidth: '1366px',
69 | margin: '0 auto',
70 | },
71 | });
72 |
73 | const LogoWrap = styled('div', {
74 | base: {
75 | display: 'flex',
76 | alignItems: 'center',
77 | gap: '14px',
78 | },
79 | });
80 |
81 | const LinkWrap = styled('div', {
82 | base: {
83 | display: 'flex',
84 | flexDirection: {
85 | base: 'column',
86 | xl: 'row',
87 | },
88 | gap: {
89 | base: '6px',
90 | xl: '31px',
91 | },
92 | },
93 | });
94 |
95 | const Link = styled('a', {
96 | base: {
97 | display: 'flex',
98 | alignItems: 'center',
99 | gap: {
100 | base: '10px',
101 | xl: '12px',
102 | },
103 | fontSize: {
104 | base: '12px',
105 | xl: '14px',
106 | },
107 | lineHeight: '140%',
108 | cursor: 'pointer',
109 | color: 'rgba(255, 255, 255, 0.7)',
110 | },
111 | });
112 |
113 | const IconWrap = styled('div', {
114 | base: {
115 | display: 'flex',
116 | justifyContent: 'center',
117 | alignItems: 'center',
118 | width: {
119 | base: '26px',
120 | xl: '28px',
121 | },
122 | height: {
123 | base: '26px',
124 | xl: '28px',
125 | },
126 | borderRadius: '8px',
127 | backgroundColor: 'rgba(78, 77, 96, 0.3)',
128 | },
129 | });
130 |
131 | const Rights = styled('span', {
132 | base: {
133 | color: 'rgba(255, 255, 255, 0.4)',
134 | textAlign: {
135 | base: 'left',
136 | xl: 'right',
137 | },
138 | fontVariantNumeric: 'lining-nums tabular-nums',
139 | fontSize: {
140 | base: '12px',
141 | xl: '14px',
142 | },
143 | fontStyle: 'normal',
144 | fontWeight: '500',
145 | lineHeight: '140%',
146 | letterSpacing: '0.28px',
147 | },
148 | });
149 |
150 | export default Footer;
151 |
--------------------------------------------------------------------------------
/src/shared/components/Footer/components/Logo.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 |
3 | const Logo: FC = () => (
4 |
49 | );
50 |
51 | export default Logo;
52 |
--------------------------------------------------------------------------------
/src/shared/components/Footer/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Logo } from './Logo';
2 |
--------------------------------------------------------------------------------
/src/shared/components/Footer/icons/index.tsx:
--------------------------------------------------------------------------------
1 | import { FC, SVGProps } from 'react';
2 |
3 | const MailIcon: FC> = props => (
4 |
20 | );
21 |
22 | const FacebookIcon: FC> = props => (
23 |
37 | );
38 |
39 | const GlobalIcon: FC> = props => (
40 |
63 | );
64 |
65 | const FAQIcon: FC> = props => (
66 |
82 | );
83 |
84 | export { FacebookIcon, FAQIcon, GlobalIcon, MailIcon };
85 |
--------------------------------------------------------------------------------
/src/shared/components/Footer/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Footer';
2 |
--------------------------------------------------------------------------------
/src/shared/components/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import { styled } from '@styled-system/jsx';
3 |
4 | import { Logo } from './components';
5 | import { Button } from '~/shared/components';
6 | import { FAQ_LINK } from '~/shared/constants';
7 |
8 | const Header: FC = () => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | 2024.8.24
16 |
17 |
18 | 서울특별시 광진구 능동로 209 | 세종대학교 광개토회관
19 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | const Container = styled('header', {
29 | base: {
30 | position: 'fixed',
31 | top: 0,
32 | left: 0,
33 | right: 0,
34 | backgroundColor: 'rgba(2, 3, 7, 0.1)',
35 | backdropFilter: 'blur(10px)',
36 | zIndex: 1,
37 | },
38 | });
39 |
40 | const Wrap = styled('div', {
41 | base: {
42 | height: {
43 | base: '60px',
44 | xl: '66px',
45 | },
46 | maxWidth: '1366px',
47 | margin: '0 auto',
48 | padding: {
49 | base: '20px',
50 | xl: '0 50px 0 80px',
51 | },
52 | display: 'flex',
53 | justifyContent: 'space-between',
54 | alignItems: 'center',
55 | },
56 | });
57 |
58 | const LogoWrap = styled('div', {
59 | base: {
60 | display: 'flex',
61 | alignItems: 'center',
62 | gap: '14px',
63 | justifyContent: {
64 | base: 'space-between',
65 | xl: 'flex-start',
66 | },
67 | width: {
68 | base: '100%',
69 | xl: 'initial',
70 | },
71 | },
72 | });
73 |
74 | const Divider = styled('div', {
75 | base: {
76 | flexGrow: {
77 | base: 1,
78 | xl: 'initial',
79 | },
80 | width: '50px',
81 | height: '1px',
82 | background: 'linear-gradient(90deg, #fff 0%, rgba(255, 255, 255, 0) 100%)',
83 | },
84 | });
85 |
86 | const Date = styled('span', {
87 | base: {
88 | color: 'rgba(255, 255, 255, 0.7)',
89 | fontSize: '14px',
90 | fontStyle: 'normal',
91 | fontWeight: '400',
92 | lineHeight: '140%',
93 | },
94 | });
95 |
96 | const ActionWrap = styled('div', {
97 | base: {
98 | display: {
99 | base: 'none',
100 | xl: 'flex',
101 | },
102 | alignItems: 'center',
103 | gap: '25px',
104 | },
105 | });
106 |
107 | const Place = styled('span', {
108 | base: {
109 | color: 'rgba(255, 255, 255, 0.7)',
110 | textAlign: 'right',
111 | fontSize: '14px',
112 | fontStyle: 'normal',
113 | fontWeight: '300',
114 | lineHeight: '140%',
115 | },
116 | });
117 |
118 | export default Header;
119 |
--------------------------------------------------------------------------------
/src/shared/components/Header/components/Logo.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 |
3 | const Logo: FC = () => (
4 |
49 | );
50 |
51 | export default Logo;
52 |
--------------------------------------------------------------------------------
/src/shared/components/Header/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Logo } from './Logo';
2 |
--------------------------------------------------------------------------------
/src/shared/components/Header/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Header';
2 |
--------------------------------------------------------------------------------
/src/shared/components/MainCTAButton/MainCTAButton.tsx:
--------------------------------------------------------------------------------
1 | import { FC, PropsWithChildren } from 'react';
2 | import { styled } from '@styled-system/jsx';
3 | import { match, P } from 'ts-pattern';
4 | import { TICKET_LINK, YOUTUBE_LINK } from '~/shared/constants';
5 |
6 | import { useEventStatus } from './hooks';
7 | import { Button } from '~/shared/components';
8 |
9 | interface Props {
10 | size: 's' | 'm';
11 | }
12 |
13 | const MainCTAButton: FC> = ({ size }) => {
14 | const eventStatus = useEventStatus();
15 | const label = match(eventStatus)
16 | .with('presale', () => '7월 31 구매 오픈')
17 | .with('sale', () => '티켓 구매하기')
18 | .with('soldout', () => 'Sold Out')
19 | .with('postevent', () => '세션 보러가기')
20 | .exhaustive();
21 | const href = match(eventStatus)
22 | .with(P.union('presale', 'sale'), () => TICKET_LINK)
23 | .with('soldout', () => TICKET_LINK)
24 | .with('postevent', () => YOUTUBE_LINK)
25 | .exhaustive();
26 | const status = match(eventStatus)
27 | .with(P.union('presale', 'soldout'), () => 'inactive' as const)
28 | .otherwise(() => 'active' as const);
29 | return (
30 |
36 | );
37 | };
38 |
39 | const SubLabel = styled('span', {
40 | base: {
41 | fontSize: '12px',
42 | color: 'rgba(255, 255, 255, 0.7)',
43 | },
44 | });
45 |
46 | export default MainCTAButton;
47 |
--------------------------------------------------------------------------------
/src/shared/components/MainCTAButton/hooks/index.ts:
--------------------------------------------------------------------------------
1 | import { match } from 'ts-pattern';
2 | import { EVENT_END_TIMESTAMP, TICKET_OPEN_TIMESTAMP } from '~/shared/constants';
3 | import { useEffect, useState } from 'react';
4 |
5 | type Status = 'presale' | 'sale' | 'soldout' | 'postevent';
6 |
7 | const getStatus = (): Status =>
8 | match(Date.now())
9 | .when(
10 | now => now < TICKET_OPEN_TIMESTAMP,
11 | () => 'presale' as const
12 | )
13 | .when(
14 | now => now > EVENT_END_TIMESTAMP,
15 | () => 'postevent' as const
16 | )
17 | .otherwise(() => 'soldout' as const);
18 |
19 | const useEventStatus = () => {
20 | const [status, setStatus] = useState(getStatus());
21 | useEffect(() => {
22 | const interval = setInterval(() => {
23 | setStatus(getStatus());
24 | }, 1000);
25 | return () => clearInterval(interval);
26 | }, [setStatus]);
27 | return status;
28 | };
29 |
30 | export { useEventStatus };
31 |
--------------------------------------------------------------------------------
/src/shared/components/MainCTAButton/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './MainCTAButton';
2 |
--------------------------------------------------------------------------------
/src/shared/components/SectionTitle.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import { styled } from '@styled-system/jsx';
3 | import FadeIn from '~/shared/components/FadeIn';
4 |
5 | interface Props {
6 | title: string;
7 | description?: string;
8 | }
9 |
10 | const SectionTitle: FC = ({ title, description }) => {
11 | return (
12 |
13 | {title}
14 | {description && (
15 |
16 |
17 |
18 | )}
19 |
20 | );
21 | };
22 |
23 | const Container = styled('div', {
24 | base: {},
25 | });
26 |
27 | const Title = styled(FadeIn, {
28 | base: {
29 | color: 'rgba(255, 255, 255, 0.6)',
30 | fontSize: {
31 | base: '18px',
32 | xl: '24px',
33 | },
34 | fontStyle: 'normal',
35 | fontWeight: '500',
36 | lineHeight: '130%',
37 | textAlign: 'center',
38 | },
39 | });
40 |
41 | const Description = styled('p', {
42 | base: {
43 | marginTop: '30px',
44 | maxWidth: '700px',
45 | color: '#fff',
46 | textWrap: 'wrap',
47 | textAlign: 'center',
48 | fontSize: {
49 | base: '20px',
50 | xl: '48px',
51 | },
52 | fontStyle: 'normal',
53 | fontWeight: '600',
54 | lineHeight: '140%',
55 | },
56 | });
57 |
58 | export default SectionTitle;
59 |
--------------------------------------------------------------------------------
/src/shared/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Button } from './Button';
2 | export { default as Column } from './Column';
3 | export { default as Header } from './Header';
4 | export { default as FadeIn } from './FadeIn';
5 | export { default as Footer } from './Footer';
6 | export { default as MainCTAButton } from './MainCTAButton';
7 | export { default as SectionTitle } from './SectionTitle';
8 |
--------------------------------------------------------------------------------
/src/shared/constants/index.ts:
--------------------------------------------------------------------------------
1 | const TICKET_LINK =
2 | 'https://booking.naver.com/booking/5/bizes/263881/items/6059588';
3 | const YOUTUBE_LINK = 'https://www.youtube.com/@feconfkorea';
4 | const FE_GROUP_LINK = 'https://www.facebook.com/feconf.kr';
5 | const LINKED_IN_LINK = 'https://www.linkedin.com/company/feconf';
6 | const FAQ_LINK =
7 | 'https://neighborly-dracopelta-8f3.notion.site/FAQ-198b5b664b9c457b8faadc1517888f94';
8 | const EMAIL = 'feconf@googlegroups.com';
9 | const TICKET_OPEN_TIMESTAMP = 1722391200000;
10 | const EVENT_END_TIMESTAMP = 1724490000000;
11 |
12 | export {
13 | TICKET_LINK,
14 | YOUTUBE_LINK,
15 | FE_GROUP_LINK,
16 | LINKED_IN_LINK,
17 | FAQ_LINK,
18 | EMAIL,
19 | TICKET_OPEN_TIMESTAMP,
20 | EVENT_END_TIMESTAMP,
21 | };
22 |
--------------------------------------------------------------------------------
/src/shared/icons/LocationIcon.tsx:
--------------------------------------------------------------------------------
1 | import { FC, SVGProps } from 'react';
2 |
3 | const LocationIcon: FC> = props => (
4 |
31 | );
32 |
33 | export default LocationIcon;
34 |
--------------------------------------------------------------------------------
/src/shared/icons/TimeIcon.tsx:
--------------------------------------------------------------------------------
1 | import { FC, SVGProps } from 'react';
2 |
3 | const TimeIcon: FC> = props => (
4 |
23 | );
24 |
25 | export default TimeIcon;
26 |
--------------------------------------------------------------------------------
/src/shared/icons/index.ts:
--------------------------------------------------------------------------------
1 | export { default as TimeIcon } from './TimeIcon';
2 | export { default as LocationIcon } from './LocationIcon';
3 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "downlevelIteration": true,
12 | "noEmit": true,
13 | "esModuleInterop": true,
14 | "module": "esnext",
15 | "moduleResolution": "bundler",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "jsx": "preserve",
19 | "incremental": true,
20 | "plugins": [
21 | {
22 | "name": "next"
23 | }
24 | ],
25 | "paths": {
26 | "~/*": [
27 | "./src/*"
28 | ],
29 | "@styled-system/*": [
30 | "./styled-system/*"
31 | ]
32 | }
33 | },
34 | "include": [
35 | "next-env.d.ts",
36 | "styled-system",
37 | "**/*.ts",
38 | "**/*.tsx",
39 | ".next/types/**/*.ts"
40 | ],
41 | "exclude": [
42 | "node_modules"
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------