diff --git a/.env b/.env index f82651ed..c6bda04c 100644 --- a/.env +++ b/.env @@ -1,2 +1,6 @@ VITE_BASE_URL=https://api.humanzipyo.com -VITE_GOOGLE_ANALYTICS=G-EZPQMV95QJ \ No newline at end of file +VITE_GOOGLE_ANALYTICS=G-EZPQMV95QJ +VITE_KAKAO_API_KEY=6c656322cc1bcb9669fbaee86b9df89a +VITE_APPLE_CLIENT_ID=humanzipyo.app.com +VITE_GOOGLE_CLIENT_ID=376774774273-5do2k4e5r3k13mgjdfam4csprajporr9.apps.googleusercontent.com +VITE_NAVER_CLIENT_ID=c943YPmMR8bflLezMJGz diff --git a/index.html b/index.html index b83c8513..9a91544f 100644 --- a/index.html +++ b/index.html @@ -21,7 +21,7 @@ 인간지표 : 주식투자심리도우미 -
- +
+ diff --git a/out.wat b/out.wat new file mode 100644 index 00000000..f4bf116f --- /dev/null +++ b/out.wat @@ -0,0 +1,3805 @@ +(module + (type (;0;) (func (result i32))) + (type (;1;) (func (param i32) (result i32))) + (type (;2;) (func)) + (type (;3;) (func (param i32))) + (type (;4;) (func (param i32 i32))) + (type (;5;) (func (param i32 i32 i32) (result i32))) + (type (;6;) (func (param i32 i32) (result i32))) + (func (;0;) (type 2) + call 10) + (func (;1;) (type 4) (param i32 i32) + (local i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) + global.get 0 + local.set 2 + i32.const 16 + local.set 3 + local.get 2 + local.get 3 + i32.sub + local.set 4 + local.get 4 + global.set 0 + local.get 4 + local.get 0 + i32.store offset=12 + local.get 4 + local.get 1 + i32.store offset=8 + local.get 4 + i32.load offset=12 + local.set 5 + i32.const 0 + local.set 6 + local.get 6 + local.get 5 + i32.store offset=67768 + local.get 4 + i32.load offset=8 + local.set 7 + i32.const 0 + local.set 8 + local.get 8 + local.get 7 + i32.store offset=67772 + i32.const 0 + local.set 9 + local.get 9 + i32.load offset=67768 + local.set 10 + i32.const 1 + local.set 11 + local.get 10 + local.get 11 + i32.add + local.set 12 + i32.const 0 + local.set 13 + local.get 13 + local.get 12 + i32.store offset=67776 + i32.const 0 + local.set 14 + local.get 14 + i32.load offset=67772 + local.set 15 + i32.const 1 + local.set 16 + local.get 15 + local.get 16 + i32.add + local.set 17 + i32.const 0 + local.set 18 + local.get 18 + local.get 17 + i32.store offset=67780 + i32.const 0 + local.set 19 + local.get 19 + i32.load offset=67776 + local.set 20 + i32.const 2 + local.set 21 + local.get 20 + local.get 21 + i32.shl + local.set 22 + i32.const 0 + local.set 23 + local.get 23 + i32.load offset=67780 + local.set 24 + local.get 22 + local.get 24 + i32.mul + local.set 25 + local.get 25 + call 4 + local.set 26 + i32.const 0 + local.set 27 + local.get 27 + local.get 26 + i32.store offset=67784 + i32.const 0 + local.set 28 + local.get 28 + i32.load offset=67768 + local.set 29 + i32.const 0 + local.set 30 + local.get 29 + local.get 30 + i32.shl + local.set 31 + i32.const 0 + local.set 32 + local.get 32 + i32.load offset=67772 + local.set 33 + local.get 31 + local.get 33 + i32.mul + local.set 34 + local.get 34 + call 4 + local.set 35 + i32.const 0 + local.set 36 + local.get 36 + local.get 35 + i32.store offset=67788 + i32.const 0 + local.set 37 + local.get 37 + i32.load offset=67768 + local.set 38 + i32.const 2 + local.set 39 + local.get 38 + local.get 39 + i32.shl + local.set 40 + i32.const 0 + local.set 41 + local.get 41 + i32.load offset=67772 + local.set 42 + local.get 40 + local.get 42 + i32.mul + local.set 43 + local.get 43 + call 4 + local.set 44 + i32.const 0 + local.set 45 + local.get 45 + local.get 44 + i32.store offset=67792 + i32.const 16 + local.set 46 + local.get 4 + local.get 46 + i32.add + local.set 47 + local.get 47 + global.set 0 + return) + (func (;2;) (type 2) + block ;; label = @1 + i32.const 1 + i32.eqz + br_if 0 (;@1;) + call 0 + end) + (func (;3;) (type 0) (result i32) + i32.const 67796) + (func (;4;) (type 1) (param i32) (result i32) + (local i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) + global.get 0 + i32.const 16 + i32.sub + local.tee 1 + global.set 0 + block ;; label = @1 + block ;; label = @2 + block ;; label = @3 + block ;; label = @4 + block ;; label = @5 + block ;; label = @6 + block ;; label = @7 + block ;; label = @8 + block ;; label = @9 + block ;; label = @10 + block ;; label = @11 + local.get 0 + i32.const 244 + i32.gt_u + br_if 0 (;@11;) + block ;; label = @12 + i32.const 0 + i32.load offset=67800 + local.tee 2 + i32.const 16 + local.get 0 + i32.const 11 + i32.add + i32.const 504 + i32.and + local.get 0 + i32.const 11 + i32.lt_u + select + local.tee 3 + i32.const 3 + i32.shr_u + local.tee 4 + i32.shr_u + local.tee 0 + i32.const 3 + i32.and + i32.eqz + br_if 0 (;@12;) + block ;; label = @13 + block ;; label = @14 + local.get 0 + i32.const -1 + i32.xor + i32.const 1 + i32.and + local.get 4 + i32.add + local.tee 3 + i32.const 3 + i32.shl + local.tee 4 + i32.const 67840 + i32.add + local.tee 0 + local.get 4 + i32.const 67848 + i32.add + i32.load + local.tee 4 + i32.load offset=8 + local.tee 5 + i32.ne + br_if 0 (;@14;) + i32.const 0 + local.get 2 + i32.const -2 + local.get 3 + i32.rotl + i32.and + i32.store offset=67800 + br 1 (;@13;) + end + local.get 5 + local.get 0 + i32.store offset=12 + local.get 0 + local.get 5 + i32.store offset=8 + end + local.get 4 + i32.const 8 + i32.add + local.set 0 + local.get 4 + local.get 3 + i32.const 3 + i32.shl + local.tee 3 + i32.const 3 + i32.or + i32.store offset=4 + local.get 4 + local.get 3 + i32.add + local.tee 4 + local.get 4 + i32.load offset=4 + i32.const 1 + i32.or + i32.store offset=4 + br 11 (;@1;) + end + local.get 3 + i32.const 0 + i32.load offset=67808 + local.tee 6 + i32.le_u + br_if 1 (;@10;) + block ;; label = @12 + local.get 0 + i32.eqz + br_if 0 (;@12;) + block ;; label = @13 + block ;; label = @14 + local.get 0 + local.get 4 + i32.shl + i32.const 2 + local.get 4 + i32.shl + local.tee 0 + i32.const 0 + local.get 0 + i32.sub + i32.or + i32.and + i32.ctz + local.tee 4 + i32.const 3 + i32.shl + local.tee 0 + i32.const 67840 + i32.add + local.tee 5 + local.get 0 + i32.const 67848 + i32.add + i32.load + local.tee 0 + i32.load offset=8 + local.tee 7 + i32.ne + br_if 0 (;@14;) + i32.const 0 + local.get 2 + i32.const -2 + local.get 4 + i32.rotl + i32.and + local.tee 2 + i32.store offset=67800 + br 1 (;@13;) + end + local.get 7 + local.get 5 + i32.store offset=12 + local.get 5 + local.get 7 + i32.store offset=8 + end + local.get 0 + local.get 3 + i32.const 3 + i32.or + i32.store offset=4 + local.get 0 + local.get 3 + i32.add + local.tee 7 + local.get 4 + i32.const 3 + i32.shl + local.tee 4 + local.get 3 + i32.sub + local.tee 3 + i32.const 1 + i32.or + i32.store offset=4 + local.get 0 + local.get 4 + i32.add + local.get 3 + i32.store + block ;; label = @13 + local.get 6 + i32.eqz + br_if 0 (;@13;) + local.get 6 + i32.const -8 + i32.and + i32.const 67840 + i32.add + local.set 5 + i32.const 0 + i32.load offset=67820 + local.set 4 + block ;; label = @14 + block ;; label = @15 + local.get 2 + i32.const 1 + local.get 6 + i32.const 3 + i32.shr_u + i32.shl + local.tee 8 + i32.and + br_if 0 (;@15;) + i32.const 0 + local.get 2 + local.get 8 + i32.or + i32.store offset=67800 + local.get 5 + local.set 8 + br 1 (;@14;) + end + local.get 5 + i32.load offset=8 + local.set 8 + end + local.get 5 + local.get 4 + i32.store offset=8 + local.get 8 + local.get 4 + i32.store offset=12 + local.get 4 + local.get 5 + i32.store offset=12 + local.get 4 + local.get 8 + i32.store offset=8 + end + local.get 0 + i32.const 8 + i32.add + local.set 0 + i32.const 0 + local.get 7 + i32.store offset=67820 + i32.const 0 + local.get 3 + i32.store offset=67808 + br 11 (;@1;) + end + i32.const 0 + i32.load offset=67804 + local.tee 9 + i32.eqz + br_if 1 (;@10;) + local.get 9 + i32.ctz + i32.const 2 + i32.shl + i32.const 68104 + i32.add + i32.load + local.tee 7 + i32.load offset=4 + i32.const -8 + i32.and + local.get 3 + i32.sub + local.set 4 + local.get 7 + local.set 5 + block ;; label = @12 + loop ;; label = @13 + block ;; label = @14 + local.get 5 + i32.load offset=16 + local.tee 0 + br_if 0 (;@14;) + local.get 5 + i32.load offset=20 + local.tee 0 + i32.eqz + br_if 2 (;@12;) + end + local.get 0 + i32.load offset=4 + i32.const -8 + i32.and + local.get 3 + i32.sub + local.tee 5 + local.get 4 + local.get 5 + local.get 4 + i32.lt_u + local.tee 5 + select + local.set 4 + local.get 0 + local.get 7 + local.get 5 + select + local.set 7 + local.get 0 + local.set 5 + br 0 (;@13;) + end + unreachable + end + local.get 7 + i32.load offset=24 + local.set 10 + block ;; label = @12 + local.get 7 + i32.load offset=12 + local.tee 0 + local.get 7 + i32.eq + br_if 0 (;@12;) + local.get 7 + i32.load offset=8 + local.tee 5 + local.get 0 + i32.store offset=12 + local.get 0 + local.get 5 + i32.store offset=8 + br 10 (;@2;) + end + block ;; label = @12 + block ;; label = @13 + local.get 7 + i32.load offset=20 + local.tee 5 + i32.eqz + br_if 0 (;@13;) + local.get 7 + i32.const 20 + i32.add + local.set 8 + br 1 (;@12;) + end + local.get 7 + i32.load offset=16 + local.tee 5 + i32.eqz + br_if 3 (;@9;) + local.get 7 + i32.const 16 + i32.add + local.set 8 + end + loop ;; label = @12 + local.get 8 + local.set 11 + local.get 5 + local.tee 0 + i32.const 20 + i32.add + local.set 8 + local.get 0 + i32.load offset=20 + local.tee 5 + br_if 0 (;@12;) + local.get 0 + i32.const 16 + i32.add + local.set 8 + local.get 0 + i32.load offset=16 + local.tee 5 + br_if 0 (;@12;) + end + local.get 11 + i32.const 0 + i32.store + br 9 (;@2;) + end + i32.const -1 + local.set 3 + local.get 0 + i32.const -65 + i32.gt_u + br_if 0 (;@10;) + local.get 0 + i32.const 11 + i32.add + local.tee 4 + i32.const -8 + i32.and + local.set 3 + i32.const 0 + i32.load offset=67804 + local.tee 10 + i32.eqz + br_if 0 (;@10;) + i32.const 31 + local.set 6 + block ;; label = @11 + local.get 0 + i32.const 16777204 + i32.gt_u + br_if 0 (;@11;) + local.get 3 + i32.const 38 + local.get 4 + i32.const 8 + i32.shr_u + i32.clz + local.tee 0 + i32.sub + i32.shr_u + i32.const 1 + i32.and + local.get 0 + i32.const 1 + i32.shl + i32.sub + i32.const 62 + i32.add + local.set 6 + end + i32.const 0 + local.get 3 + i32.sub + local.set 4 + block ;; label = @11 + block ;; label = @12 + block ;; label = @13 + block ;; label = @14 + local.get 6 + i32.const 2 + i32.shl + i32.const 68104 + i32.add + i32.load + local.tee 5 + br_if 0 (;@14;) + i32.const 0 + local.set 0 + i32.const 0 + local.set 8 + br 1 (;@13;) + end + i32.const 0 + local.set 0 + local.get 3 + i32.const 0 + i32.const 25 + local.get 6 + i32.const 1 + i32.shr_u + i32.sub + local.get 6 + i32.const 31 + i32.eq + select + i32.shl + local.set 7 + i32.const 0 + local.set 8 + loop ;; label = @14 + block ;; label = @15 + local.get 5 + i32.load offset=4 + i32.const -8 + i32.and + local.get 3 + i32.sub + local.tee 2 + local.get 4 + i32.ge_u + br_if 0 (;@15;) + local.get 2 + local.set 4 + local.get 5 + local.set 8 + local.get 2 + br_if 0 (;@15;) + i32.const 0 + local.set 4 + local.get 5 + local.set 8 + local.get 5 + local.set 0 + br 3 (;@12;) + end + local.get 0 + local.get 5 + i32.load offset=20 + local.tee 2 + local.get 2 + local.get 5 + local.get 7 + i32.const 29 + i32.shr_u + i32.const 4 + i32.and + i32.add + i32.load offset=16 + local.tee 11 + i32.eq + select + local.get 0 + local.get 2 + select + local.set 0 + local.get 7 + i32.const 1 + i32.shl + local.set 7 + local.get 11 + local.set 5 + local.get 11 + br_if 0 (;@14;) + end + end + block ;; label = @13 + local.get 0 + local.get 8 + i32.or + br_if 0 (;@13;) + i32.const 0 + local.set 8 + i32.const 2 + local.get 6 + i32.shl + local.tee 0 + i32.const 0 + local.get 0 + i32.sub + i32.or + local.get 10 + i32.and + local.tee 0 + i32.eqz + br_if 3 (;@10;) + local.get 0 + i32.ctz + i32.const 2 + i32.shl + i32.const 68104 + i32.add + i32.load + local.set 0 + end + local.get 0 + i32.eqz + br_if 1 (;@11;) + end + loop ;; label = @12 + local.get 0 + i32.load offset=4 + i32.const -8 + i32.and + local.get 3 + i32.sub + local.tee 2 + local.get 4 + i32.lt_u + local.set 7 + block ;; label = @13 + local.get 0 + i32.load offset=16 + local.tee 5 + br_if 0 (;@13;) + local.get 0 + i32.load offset=20 + local.set 5 + end + local.get 2 + local.get 4 + local.get 7 + select + local.set 4 + local.get 0 + local.get 8 + local.get 7 + select + local.set 8 + local.get 5 + local.set 0 + local.get 5 + br_if 0 (;@12;) + end + end + local.get 8 + i32.eqz + br_if 0 (;@10;) + local.get 4 + i32.const 0 + i32.load offset=67808 + local.get 3 + i32.sub + i32.ge_u + br_if 0 (;@10;) + local.get 8 + i32.load offset=24 + local.set 11 + block ;; label = @11 + local.get 8 + i32.load offset=12 + local.tee 0 + local.get 8 + i32.eq + br_if 0 (;@11;) + local.get 8 + i32.load offset=8 + local.tee 5 + local.get 0 + i32.store offset=12 + local.get 0 + local.get 5 + i32.store offset=8 + br 8 (;@3;) + end + block ;; label = @11 + block ;; label = @12 + local.get 8 + i32.load offset=20 + local.tee 5 + i32.eqz + br_if 0 (;@12;) + local.get 8 + i32.const 20 + i32.add + local.set 7 + br 1 (;@11;) + end + local.get 8 + i32.load offset=16 + local.tee 5 + i32.eqz + br_if 3 (;@8;) + local.get 8 + i32.const 16 + i32.add + local.set 7 + end + loop ;; label = @11 + local.get 7 + local.set 2 + local.get 5 + local.tee 0 + i32.const 20 + i32.add + local.set 7 + local.get 0 + i32.load offset=20 + local.tee 5 + br_if 0 (;@11;) + local.get 0 + i32.const 16 + i32.add + local.set 7 + local.get 0 + i32.load offset=16 + local.tee 5 + br_if 0 (;@11;) + end + local.get 2 + i32.const 0 + i32.store + br 7 (;@3;) + end + block ;; label = @10 + i32.const 0 + i32.load offset=67808 + local.tee 0 + local.get 3 + i32.lt_u + br_if 0 (;@10;) + i32.const 0 + i32.load offset=67820 + local.set 4 + block ;; label = @11 + block ;; label = @12 + local.get 0 + local.get 3 + i32.sub + local.tee 5 + i32.const 16 + i32.lt_u + br_if 0 (;@12;) + local.get 4 + local.get 3 + i32.add + local.tee 7 + local.get 5 + i32.const 1 + i32.or + i32.store offset=4 + local.get 4 + local.get 0 + i32.add + local.get 5 + i32.store + local.get 4 + local.get 3 + i32.const 3 + i32.or + i32.store offset=4 + br 1 (;@11;) + end + local.get 4 + local.get 0 + i32.const 3 + i32.or + i32.store offset=4 + local.get 4 + local.get 0 + i32.add + local.tee 0 + local.get 0 + i32.load offset=4 + i32.const 1 + i32.or + i32.store offset=4 + i32.const 0 + local.set 7 + i32.const 0 + local.set 5 + end + i32.const 0 + local.get 5 + i32.store offset=67808 + i32.const 0 + local.get 7 + i32.store offset=67820 + local.get 4 + i32.const 8 + i32.add + local.set 0 + br 9 (;@1;) + end + block ;; label = @10 + i32.const 0 + i32.load offset=67812 + local.tee 7 + local.get 3 + i32.le_u + br_if 0 (;@10;) + i32.const 0 + local.get 7 + local.get 3 + i32.sub + local.tee 4 + i32.store offset=67812 + i32.const 0 + i32.const 0 + i32.load offset=67824 + local.tee 0 + local.get 3 + i32.add + local.tee 5 + i32.store offset=67824 + local.get 5 + local.get 4 + i32.const 1 + i32.or + i32.store offset=4 + local.get 0 + local.get 3 + i32.const 3 + i32.or + i32.store offset=4 + local.get 0 + i32.const 8 + i32.add + local.set 0 + br 9 (;@1;) + end + block ;; label = @10 + block ;; label = @11 + i32.const 0 + i32.load offset=68272 + i32.eqz + br_if 0 (;@11;) + i32.const 0 + i32.load offset=68280 + local.set 4 + br 1 (;@10;) + end + i32.const 0 + i64.const -1 + i64.store offset=68284 align=4 + i32.const 0 + i64.const 17592186048512 + i64.store offset=68276 align=4 + i32.const 0 + local.get 1 + i32.const 12 + i32.add + i32.const -16 + i32.and + i32.const 1431655768 + i32.xor + i32.store offset=68272 + i32.const 0 + i32.const 0 + i32.store offset=68292 + i32.const 0 + i32.const 0 + i32.store offset=68244 + i32.const 4096 + local.set 4 + end + i32.const 0 + local.set 0 + local.get 4 + local.get 3 + i32.const 47 + i32.add + local.tee 6 + i32.add + local.tee 2 + i32.const 0 + local.get 4 + i32.sub + local.tee 11 + i32.and + local.tee 8 + local.get 3 + i32.le_u + br_if 8 (;@1;) + i32.const 0 + local.set 0 + block ;; label = @10 + i32.const 0 + i32.load offset=68240 + local.tee 4 + i32.eqz + br_if 0 (;@10;) + i32.const 0 + i32.load offset=68232 + local.tee 5 + local.get 8 + i32.add + local.tee 10 + local.get 5 + i32.le_u + br_if 9 (;@1;) + local.get 10 + local.get 4 + i32.gt_u + br_if 9 (;@1;) + end + block ;; label = @10 + block ;; label = @11 + i32.const 0 + i32.load8_u offset=68244 + i32.const 4 + i32.and + br_if 0 (;@11;) + block ;; label = @12 + block ;; label = @13 + block ;; label = @14 + block ;; label = @15 + block ;; label = @16 + i32.const 0 + i32.load offset=67824 + local.tee 4 + i32.eqz + br_if 0 (;@16;) + i32.const 68248 + local.set 0 + loop ;; label = @17 + block ;; label = @18 + local.get 4 + local.get 0 + i32.load + local.tee 5 + i32.lt_u + br_if 0 (;@18;) + local.get 4 + local.get 5 + local.get 0 + i32.load offset=4 + i32.add + i32.lt_u + br_if 3 (;@15;) + end + local.get 0 + i32.load offset=8 + local.tee 0 + br_if 0 (;@17;) + end + end + i32.const 0 + call 9 + local.tee 7 + i32.const -1 + i32.eq + br_if 3 (;@12;) + local.get 8 + local.set 2 + block ;; label = @16 + i32.const 0 + i32.load offset=68276 + local.tee 0 + i32.const -1 + i32.add + local.tee 4 + local.get 7 + i32.and + i32.eqz + br_if 0 (;@16;) + local.get 8 + local.get 7 + i32.sub + local.get 4 + local.get 7 + i32.add + i32.const 0 + local.get 0 + i32.sub + i32.and + i32.add + local.set 2 + end + local.get 2 + local.get 3 + i32.le_u + br_if 3 (;@12;) + block ;; label = @16 + i32.const 0 + i32.load offset=68240 + local.tee 0 + i32.eqz + br_if 0 (;@16;) + i32.const 0 + i32.load offset=68232 + local.tee 4 + local.get 2 + i32.add + local.tee 5 + local.get 4 + i32.le_u + br_if 4 (;@12;) + local.get 5 + local.get 0 + i32.gt_u + br_if 4 (;@12;) + end + local.get 2 + call 9 + local.tee 0 + local.get 7 + i32.ne + br_if 1 (;@14;) + br 5 (;@10;) + end + local.get 2 + local.get 7 + i32.sub + local.get 11 + i32.and + local.tee 2 + call 9 + local.tee 7 + local.get 0 + i32.load + local.get 0 + i32.load offset=4 + i32.add + i32.eq + br_if 1 (;@13;) + local.get 7 + local.set 0 + end + local.get 0 + i32.const -1 + i32.eq + br_if 1 (;@12;) + block ;; label = @14 + local.get 2 + local.get 3 + i32.const 48 + i32.add + i32.lt_u + br_if 0 (;@14;) + local.get 0 + local.set 7 + br 4 (;@10;) + end + local.get 6 + local.get 2 + i32.sub + i32.const 0 + i32.load offset=68280 + local.tee 4 + i32.add + i32.const 0 + local.get 4 + i32.sub + i32.and + local.tee 4 + call 9 + i32.const -1 + i32.eq + br_if 1 (;@12;) + local.get 4 + local.get 2 + i32.add + local.set 2 + local.get 0 + local.set 7 + br 3 (;@10;) + end + local.get 7 + i32.const -1 + i32.ne + br_if 2 (;@10;) + end + i32.const 0 + i32.const 0 + i32.load offset=68244 + i32.const 4 + i32.or + i32.store offset=68244 + end + local.get 8 + call 9 + local.set 7 + i32.const 0 + call 9 + local.set 0 + local.get 7 + i32.const -1 + i32.eq + br_if 5 (;@5;) + local.get 0 + i32.const -1 + i32.eq + br_if 5 (;@5;) + local.get 7 + local.get 0 + i32.ge_u + br_if 5 (;@5;) + local.get 0 + local.get 7 + i32.sub + local.tee 2 + local.get 3 + i32.const 40 + i32.add + i32.le_u + br_if 5 (;@5;) + end + i32.const 0 + i32.const 0 + i32.load offset=68232 + local.get 2 + i32.add + local.tee 0 + i32.store offset=68232 + block ;; label = @10 + local.get 0 + i32.const 0 + i32.load offset=68236 + i32.le_u + br_if 0 (;@10;) + i32.const 0 + local.get 0 + i32.store offset=68236 + end + block ;; label = @10 + block ;; label = @11 + i32.const 0 + i32.load offset=67824 + local.tee 4 + i32.eqz + br_if 0 (;@11;) + i32.const 68248 + local.set 0 + loop ;; label = @12 + local.get 7 + local.get 0 + i32.load + local.tee 5 + local.get 0 + i32.load offset=4 + local.tee 8 + i32.add + i32.eq + br_if 2 (;@10;) + local.get 0 + i32.load offset=8 + local.tee 0 + br_if 0 (;@12;) + br 5 (;@7;) + end + unreachable + end + block ;; label = @11 + block ;; label = @12 + i32.const 0 + i32.load offset=67816 + local.tee 0 + i32.eqz + br_if 0 (;@12;) + local.get 7 + local.get 0 + i32.ge_u + br_if 1 (;@11;) + end + i32.const 0 + local.get 7 + i32.store offset=67816 + end + i32.const 0 + local.set 0 + i32.const 0 + local.get 2 + i32.store offset=68252 + i32.const 0 + local.get 7 + i32.store offset=68248 + i32.const 0 + i32.const -1 + i32.store offset=67832 + i32.const 0 + i32.const 0 + i32.load offset=68272 + i32.store offset=67836 + i32.const 0 + i32.const 0 + i32.store offset=68260 + loop ;; label = @11 + local.get 0 + i32.const 3 + i32.shl + local.tee 4 + i32.const 67848 + i32.add + local.get 4 + i32.const 67840 + i32.add + local.tee 5 + i32.store + local.get 4 + i32.const 67852 + i32.add + local.get 5 + i32.store + local.get 0 + i32.const 1 + i32.add + local.tee 0 + i32.const 32 + i32.ne + br_if 0 (;@11;) + end + i32.const 0 + local.get 2 + i32.const -40 + i32.add + local.tee 0 + i32.const -8 + local.get 7 + i32.sub + i32.const 7 + i32.and + local.tee 4 + i32.sub + local.tee 5 + i32.store offset=67812 + i32.const 0 + local.get 7 + local.get 4 + i32.add + local.tee 4 + i32.store offset=67824 + local.get 4 + local.get 5 + i32.const 1 + i32.or + i32.store offset=4 + local.get 7 + local.get 0 + i32.add + i32.const 40 + i32.store offset=4 + i32.const 0 + i32.const 0 + i32.load offset=68288 + i32.store offset=67828 + br 4 (;@6;) + end + local.get 4 + local.get 7 + i32.ge_u + br_if 2 (;@7;) + local.get 4 + local.get 5 + i32.lt_u + br_if 2 (;@7;) + local.get 0 + i32.load offset=12 + i32.const 8 + i32.and + br_if 2 (;@7;) + local.get 0 + local.get 8 + local.get 2 + i32.add + i32.store offset=4 + i32.const 0 + local.get 4 + i32.const -8 + local.get 4 + i32.sub + i32.const 7 + i32.and + local.tee 0 + i32.add + local.tee 5 + i32.store offset=67824 + i32.const 0 + i32.const 0 + i32.load offset=67812 + local.get 2 + i32.add + local.tee 7 + local.get 0 + i32.sub + local.tee 0 + i32.store offset=67812 + local.get 5 + local.get 0 + i32.const 1 + i32.or + i32.store offset=4 + local.get 4 + local.get 7 + i32.add + i32.const 40 + i32.store offset=4 + i32.const 0 + i32.const 0 + i32.load offset=68288 + i32.store offset=67828 + br 3 (;@6;) + end + i32.const 0 + local.set 0 + br 6 (;@2;) + end + i32.const 0 + local.set 0 + br 4 (;@3;) + end + block ;; label = @7 + local.get 7 + i32.const 0 + i32.load offset=67816 + i32.ge_u + br_if 0 (;@7;) + i32.const 0 + local.get 7 + i32.store offset=67816 + end + local.get 7 + local.get 2 + i32.add + local.set 5 + i32.const 68248 + local.set 0 + block ;; label = @7 + block ;; label = @8 + loop ;; label = @9 + local.get 0 + i32.load + local.tee 8 + local.get 5 + i32.eq + br_if 1 (;@8;) + local.get 0 + i32.load offset=8 + local.tee 0 + br_if 0 (;@9;) + br 2 (;@7;) + end + unreachable + end + local.get 0 + i32.load8_u offset=12 + i32.const 8 + i32.and + i32.eqz + br_if 3 (;@4;) + end + i32.const 68248 + local.set 0 + block ;; label = @7 + loop ;; label = @8 + block ;; label = @9 + local.get 4 + local.get 0 + i32.load + local.tee 5 + i32.lt_u + br_if 0 (;@9;) + local.get 4 + local.get 5 + local.get 0 + i32.load offset=4 + i32.add + local.tee 5 + i32.lt_u + br_if 2 (;@7;) + end + local.get 0 + i32.load offset=8 + local.set 0 + br 0 (;@8;) + end + unreachable + end + i32.const 0 + local.get 2 + i32.const -40 + i32.add + local.tee 0 + i32.const -8 + local.get 7 + i32.sub + i32.const 7 + i32.and + local.tee 8 + i32.sub + local.tee 11 + i32.store offset=67812 + i32.const 0 + local.get 7 + local.get 8 + i32.add + local.tee 8 + i32.store offset=67824 + local.get 8 + local.get 11 + i32.const 1 + i32.or + i32.store offset=4 + local.get 7 + local.get 0 + i32.add + i32.const 40 + i32.store offset=4 + i32.const 0 + i32.const 0 + i32.load offset=68288 + i32.store offset=67828 + local.get 4 + local.get 5 + i32.const 39 + local.get 5 + i32.sub + i32.const 7 + i32.and + i32.add + i32.const -47 + i32.add + local.tee 0 + local.get 0 + local.get 4 + i32.const 16 + i32.add + i32.lt_u + select + local.tee 8 + i32.const 27 + i32.store offset=4 + local.get 8 + i32.const 16 + i32.add + i32.const 0 + i64.load offset=68256 align=4 + i64.store align=4 + local.get 8 + i32.const 0 + i64.load offset=68248 align=4 + i64.store offset=8 align=4 + i32.const 0 + local.get 8 + i32.const 8 + i32.add + i32.store offset=68256 + i32.const 0 + local.get 2 + i32.store offset=68252 + i32.const 0 + local.get 7 + i32.store offset=68248 + i32.const 0 + i32.const 0 + i32.store offset=68260 + local.get 8 + i32.const 24 + i32.add + local.set 0 + loop ;; label = @7 + local.get 0 + i32.const 7 + i32.store offset=4 + local.get 0 + i32.const 8 + i32.add + local.set 7 + local.get 0 + i32.const 4 + i32.add + local.set 0 + local.get 7 + local.get 5 + i32.lt_u + br_if 0 (;@7;) + end + local.get 8 + local.get 4 + i32.eq + br_if 0 (;@6;) + local.get 8 + local.get 8 + i32.load offset=4 + i32.const -2 + i32.and + i32.store offset=4 + local.get 4 + local.get 8 + local.get 4 + i32.sub + local.tee 7 + i32.const 1 + i32.or + i32.store offset=4 + local.get 8 + local.get 7 + i32.store + block ;; label = @7 + block ;; label = @8 + local.get 7 + i32.const 255 + i32.gt_u + br_if 0 (;@8;) + local.get 7 + i32.const -8 + i32.and + i32.const 67840 + i32.add + local.set 0 + block ;; label = @9 + block ;; label = @10 + i32.const 0 + i32.load offset=67800 + local.tee 5 + i32.const 1 + local.get 7 + i32.const 3 + i32.shr_u + i32.shl + local.tee 7 + i32.and + br_if 0 (;@10;) + i32.const 0 + local.get 5 + local.get 7 + i32.or + i32.store offset=67800 + local.get 0 + local.set 5 + br 1 (;@9;) + end + local.get 0 + i32.load offset=8 + local.set 5 + end + local.get 0 + local.get 4 + i32.store offset=8 + local.get 5 + local.get 4 + i32.store offset=12 + i32.const 12 + local.set 7 + i32.const 8 + local.set 8 + br 1 (;@7;) + end + i32.const 31 + local.set 0 + block ;; label = @8 + local.get 7 + i32.const 16777215 + i32.gt_u + br_if 0 (;@8;) + local.get 7 + i32.const 38 + local.get 7 + i32.const 8 + i32.shr_u + i32.clz + local.tee 0 + i32.sub + i32.shr_u + i32.const 1 + i32.and + local.get 0 + i32.const 1 + i32.shl + i32.sub + i32.const 62 + i32.add + local.set 0 + end + local.get 4 + local.get 0 + i32.store offset=28 + local.get 4 + i64.const 0 + i64.store offset=16 align=4 + local.get 0 + i32.const 2 + i32.shl + i32.const 68104 + i32.add + local.set 5 + block ;; label = @8 + block ;; label = @9 + block ;; label = @10 + i32.const 0 + i32.load offset=67804 + local.tee 8 + i32.const 1 + local.get 0 + i32.shl + local.tee 2 + i32.and + br_if 0 (;@10;) + i32.const 0 + local.get 8 + local.get 2 + i32.or + i32.store offset=67804 + local.get 5 + local.get 4 + i32.store + local.get 4 + local.get 5 + i32.store offset=24 + br 1 (;@9;) + end + local.get 7 + i32.const 0 + i32.const 25 + local.get 0 + i32.const 1 + i32.shr_u + i32.sub + local.get 0 + i32.const 31 + i32.eq + select + i32.shl + local.set 0 + local.get 5 + i32.load + local.set 8 + loop ;; label = @10 + local.get 8 + local.tee 5 + i32.load offset=4 + i32.const -8 + i32.and + local.get 7 + i32.eq + br_if 2 (;@8;) + local.get 0 + i32.const 29 + i32.shr_u + local.set 8 + local.get 0 + i32.const 1 + i32.shl + local.set 0 + local.get 5 + local.get 8 + i32.const 4 + i32.and + i32.add + local.tee 2 + i32.load offset=16 + local.tee 8 + br_if 0 (;@10;) + end + local.get 2 + i32.const 16 + i32.add + local.get 4 + i32.store + local.get 4 + local.get 5 + i32.store offset=24 + end + i32.const 8 + local.set 7 + i32.const 12 + local.set 8 + local.get 4 + local.set 5 + local.get 4 + local.set 0 + br 1 (;@7;) + end + local.get 5 + i32.load offset=8 + local.tee 0 + local.get 4 + i32.store offset=12 + local.get 5 + local.get 4 + i32.store offset=8 + local.get 4 + local.get 0 + i32.store offset=8 + i32.const 0 + local.set 0 + i32.const 24 + local.set 7 + i32.const 12 + local.set 8 + end + local.get 4 + local.get 8 + i32.add + local.get 5 + i32.store + local.get 4 + local.get 7 + i32.add + local.get 0 + i32.store + end + i32.const 0 + i32.load offset=67812 + local.tee 0 + local.get 3 + i32.le_u + br_if 0 (;@5;) + i32.const 0 + local.get 0 + local.get 3 + i32.sub + local.tee 4 + i32.store offset=67812 + i32.const 0 + i32.const 0 + i32.load offset=67824 + local.tee 0 + local.get 3 + i32.add + local.tee 5 + i32.store offset=67824 + local.get 5 + local.get 4 + i32.const 1 + i32.or + i32.store offset=4 + local.get 0 + local.get 3 + i32.const 3 + i32.or + i32.store offset=4 + local.get 0 + i32.const 8 + i32.add + local.set 0 + br 4 (;@1;) + end + call 3 + i32.const 48 + i32.store + i32.const 0 + local.set 0 + br 3 (;@1;) + end + local.get 0 + local.get 7 + i32.store + local.get 0 + local.get 0 + i32.load offset=4 + local.get 2 + i32.add + i32.store offset=4 + local.get 7 + local.get 8 + local.get 3 + call 5 + local.set 0 + br 2 (;@1;) + end + block ;; label = @3 + local.get 11 + i32.eqz + br_if 0 (;@3;) + block ;; label = @4 + block ;; label = @5 + local.get 8 + local.get 8 + i32.load offset=28 + local.tee 7 + i32.const 2 + i32.shl + i32.const 68104 + i32.add + local.tee 5 + i32.load + i32.ne + br_if 0 (;@5;) + local.get 5 + local.get 0 + i32.store + local.get 0 + br_if 1 (;@4;) + i32.const 0 + local.get 10 + i32.const -2 + local.get 7 + i32.rotl + i32.and + local.tee 10 + i32.store offset=67804 + br 2 (;@3;) + end + block ;; label = @5 + block ;; label = @6 + local.get 11 + i32.load offset=16 + local.get 8 + i32.ne + br_if 0 (;@6;) + local.get 11 + local.get 0 + i32.store offset=16 + br 1 (;@5;) + end + local.get 11 + local.get 0 + i32.store offset=20 + end + local.get 0 + i32.eqz + br_if 1 (;@3;) + end + local.get 0 + local.get 11 + i32.store offset=24 + block ;; label = @4 + local.get 8 + i32.load offset=16 + local.tee 5 + i32.eqz + br_if 0 (;@4;) + local.get 0 + local.get 5 + i32.store offset=16 + local.get 5 + local.get 0 + i32.store offset=24 + end + local.get 8 + i32.load offset=20 + local.tee 5 + i32.eqz + br_if 0 (;@3;) + local.get 0 + local.get 5 + i32.store offset=20 + local.get 5 + local.get 0 + i32.store offset=24 + end + block ;; label = @3 + block ;; label = @4 + local.get 4 + i32.const 15 + i32.gt_u + br_if 0 (;@4;) + local.get 8 + local.get 4 + local.get 3 + i32.add + local.tee 0 + i32.const 3 + i32.or + i32.store offset=4 + local.get 8 + local.get 0 + i32.add + local.tee 0 + local.get 0 + i32.load offset=4 + i32.const 1 + i32.or + i32.store offset=4 + br 1 (;@3;) + end + local.get 8 + local.get 3 + i32.const 3 + i32.or + i32.store offset=4 + local.get 8 + local.get 3 + i32.add + local.tee 7 + local.get 4 + i32.const 1 + i32.or + i32.store offset=4 + local.get 7 + local.get 4 + i32.add + local.get 4 + i32.store + block ;; label = @4 + local.get 4 + i32.const 255 + i32.gt_u + br_if 0 (;@4;) + local.get 4 + i32.const -8 + i32.and + i32.const 67840 + i32.add + local.set 0 + block ;; label = @5 + block ;; label = @6 + i32.const 0 + i32.load offset=67800 + local.tee 3 + i32.const 1 + local.get 4 + i32.const 3 + i32.shr_u + i32.shl + local.tee 4 + i32.and + br_if 0 (;@6;) + i32.const 0 + local.get 3 + local.get 4 + i32.or + i32.store offset=67800 + local.get 0 + local.set 4 + br 1 (;@5;) + end + local.get 0 + i32.load offset=8 + local.set 4 + end + local.get 0 + local.get 7 + i32.store offset=8 + local.get 4 + local.get 7 + i32.store offset=12 + local.get 7 + local.get 0 + i32.store offset=12 + local.get 7 + local.get 4 + i32.store offset=8 + br 1 (;@3;) + end + i32.const 31 + local.set 0 + block ;; label = @4 + local.get 4 + i32.const 16777215 + i32.gt_u + br_if 0 (;@4;) + local.get 4 + i32.const 38 + local.get 4 + i32.const 8 + i32.shr_u + i32.clz + local.tee 0 + i32.sub + i32.shr_u + i32.const 1 + i32.and + local.get 0 + i32.const 1 + i32.shl + i32.sub + i32.const 62 + i32.add + local.set 0 + end + local.get 7 + local.get 0 + i32.store offset=28 + local.get 7 + i64.const 0 + i64.store offset=16 align=4 + local.get 0 + i32.const 2 + i32.shl + i32.const 68104 + i32.add + local.set 3 + block ;; label = @4 + block ;; label = @5 + block ;; label = @6 + local.get 10 + i32.const 1 + local.get 0 + i32.shl + local.tee 5 + i32.and + br_if 0 (;@6;) + i32.const 0 + local.get 10 + local.get 5 + i32.or + i32.store offset=67804 + local.get 3 + local.get 7 + i32.store + local.get 7 + local.get 3 + i32.store offset=24 + br 1 (;@5;) + end + local.get 4 + i32.const 0 + i32.const 25 + local.get 0 + i32.const 1 + i32.shr_u + i32.sub + local.get 0 + i32.const 31 + i32.eq + select + i32.shl + local.set 0 + local.get 3 + i32.load + local.set 5 + loop ;; label = @6 + local.get 5 + local.tee 3 + i32.load offset=4 + i32.const -8 + i32.and + local.get 4 + i32.eq + br_if 2 (;@4;) + local.get 0 + i32.const 29 + i32.shr_u + local.set 5 + local.get 0 + i32.const 1 + i32.shl + local.set 0 + local.get 3 + local.get 5 + i32.const 4 + i32.and + i32.add + local.tee 2 + i32.load offset=16 + local.tee 5 + br_if 0 (;@6;) + end + local.get 2 + i32.const 16 + i32.add + local.get 7 + i32.store + local.get 7 + local.get 3 + i32.store offset=24 + end + local.get 7 + local.get 7 + i32.store offset=12 + local.get 7 + local.get 7 + i32.store offset=8 + br 1 (;@3;) + end + local.get 3 + i32.load offset=8 + local.tee 0 + local.get 7 + i32.store offset=12 + local.get 3 + local.get 7 + i32.store offset=8 + local.get 7 + i32.const 0 + i32.store offset=24 + local.get 7 + local.get 3 + i32.store offset=12 + local.get 7 + local.get 0 + i32.store offset=8 + end + local.get 8 + i32.const 8 + i32.add + local.set 0 + br 1 (;@1;) + end + block ;; label = @2 + local.get 10 + i32.eqz + br_if 0 (;@2;) + block ;; label = @3 + block ;; label = @4 + local.get 7 + local.get 7 + i32.load offset=28 + local.tee 8 + i32.const 2 + i32.shl + i32.const 68104 + i32.add + local.tee 5 + i32.load + i32.ne + br_if 0 (;@4;) + local.get 5 + local.get 0 + i32.store + local.get 0 + br_if 1 (;@3;) + i32.const 0 + local.get 9 + i32.const -2 + local.get 8 + i32.rotl + i32.and + i32.store offset=67804 + br 2 (;@2;) + end + block ;; label = @4 + block ;; label = @5 + local.get 10 + i32.load offset=16 + local.get 7 + i32.ne + br_if 0 (;@5;) + local.get 10 + local.get 0 + i32.store offset=16 + br 1 (;@4;) + end + local.get 10 + local.get 0 + i32.store offset=20 + end + local.get 0 + i32.eqz + br_if 1 (;@2;) + end + local.get 0 + local.get 10 + i32.store offset=24 + block ;; label = @3 + local.get 7 + i32.load offset=16 + local.tee 5 + i32.eqz + br_if 0 (;@3;) + local.get 0 + local.get 5 + i32.store offset=16 + local.get 5 + local.get 0 + i32.store offset=24 + end + local.get 7 + i32.load offset=20 + local.tee 5 + i32.eqz + br_if 0 (;@2;) + local.get 0 + local.get 5 + i32.store offset=20 + local.get 5 + local.get 0 + i32.store offset=24 + end + block ;; label = @2 + block ;; label = @3 + local.get 4 + i32.const 15 + i32.gt_u + br_if 0 (;@3;) + local.get 7 + local.get 4 + local.get 3 + i32.add + local.tee 0 + i32.const 3 + i32.or + i32.store offset=4 + local.get 7 + local.get 0 + i32.add + local.tee 0 + local.get 0 + i32.load offset=4 + i32.const 1 + i32.or + i32.store offset=4 + br 1 (;@2;) + end + local.get 7 + local.get 3 + i32.const 3 + i32.or + i32.store offset=4 + local.get 7 + local.get 3 + i32.add + local.tee 3 + local.get 4 + i32.const 1 + i32.or + i32.store offset=4 + local.get 3 + local.get 4 + i32.add + local.get 4 + i32.store + block ;; label = @3 + local.get 6 + i32.eqz + br_if 0 (;@3;) + local.get 6 + i32.const -8 + i32.and + i32.const 67840 + i32.add + local.set 5 + i32.const 0 + i32.load offset=67820 + local.set 0 + block ;; label = @4 + block ;; label = @5 + i32.const 1 + local.get 6 + i32.const 3 + i32.shr_u + i32.shl + local.tee 8 + local.get 2 + i32.and + br_if 0 (;@5;) + i32.const 0 + local.get 8 + local.get 2 + i32.or + i32.store offset=67800 + local.get 5 + local.set 8 + br 1 (;@4;) + end + local.get 5 + i32.load offset=8 + local.set 8 + end + local.get 5 + local.get 0 + i32.store offset=8 + local.get 8 + local.get 0 + i32.store offset=12 + local.get 0 + local.get 5 + i32.store offset=12 + local.get 0 + local.get 8 + i32.store offset=8 + end + i32.const 0 + local.get 3 + i32.store offset=67820 + i32.const 0 + local.get 4 + i32.store offset=67808 + end + local.get 7 + i32.const 8 + i32.add + local.set 0 + end + local.get 1 + i32.const 16 + i32.add + global.set 0 + local.get 0) + (func (;5;) (type 5) (param i32 i32 i32) (result i32) + (local i32 i32 i32 i32 i32 i32 i32) + local.get 0 + i32.const -8 + local.get 0 + i32.sub + i32.const 7 + i32.and + i32.add + local.tee 3 + local.get 2 + i32.const 3 + i32.or + i32.store offset=4 + local.get 1 + i32.const -8 + local.get 1 + i32.sub + i32.const 7 + i32.and + i32.add + local.tee 4 + local.get 3 + local.get 2 + i32.add + local.tee 5 + i32.sub + local.set 0 + block ;; label = @1 + block ;; label = @2 + local.get 4 + i32.const 0 + i32.load offset=67824 + i32.ne + br_if 0 (;@2;) + i32.const 0 + local.get 5 + i32.store offset=67824 + i32.const 0 + i32.const 0 + i32.load offset=67812 + local.get 0 + i32.add + local.tee 2 + i32.store offset=67812 + local.get 5 + local.get 2 + i32.const 1 + i32.or + i32.store offset=4 + br 1 (;@1;) + end + block ;; label = @2 + local.get 4 + i32.const 0 + i32.load offset=67820 + i32.ne + br_if 0 (;@2;) + i32.const 0 + local.get 5 + i32.store offset=67820 + i32.const 0 + i32.const 0 + i32.load offset=67808 + local.get 0 + i32.add + local.tee 2 + i32.store offset=67808 + local.get 5 + local.get 2 + i32.const 1 + i32.or + i32.store offset=4 + local.get 5 + local.get 2 + i32.add + local.get 2 + i32.store + br 1 (;@1;) + end + block ;; label = @2 + local.get 4 + i32.load offset=4 + local.tee 1 + i32.const 3 + i32.and + i32.const 1 + i32.ne + br_if 0 (;@2;) + local.get 1 + i32.const -8 + i32.and + local.set 6 + local.get 4 + i32.load offset=12 + local.set 2 + block ;; label = @3 + block ;; label = @4 + local.get 1 + i32.const 255 + i32.gt_u + br_if 0 (;@4;) + block ;; label = @5 + local.get 2 + local.get 4 + i32.load offset=8 + local.tee 7 + i32.ne + br_if 0 (;@5;) + i32.const 0 + i32.const 0 + i32.load offset=67800 + i32.const -2 + local.get 1 + i32.const 3 + i32.shr_u + i32.rotl + i32.and + i32.store offset=67800 + br 2 (;@3;) + end + local.get 7 + local.get 2 + i32.store offset=12 + local.get 2 + local.get 7 + i32.store offset=8 + br 1 (;@3;) + end + local.get 4 + i32.load offset=24 + local.set 8 + block ;; label = @4 + block ;; label = @5 + local.get 2 + local.get 4 + i32.eq + br_if 0 (;@5;) + local.get 4 + i32.load offset=8 + local.tee 1 + local.get 2 + i32.store offset=12 + local.get 2 + local.get 1 + i32.store offset=8 + br 1 (;@4;) + end + block ;; label = @5 + block ;; label = @6 + block ;; label = @7 + local.get 4 + i32.load offset=20 + local.tee 1 + i32.eqz + br_if 0 (;@7;) + local.get 4 + i32.const 20 + i32.add + local.set 7 + br 1 (;@6;) + end + local.get 4 + i32.load offset=16 + local.tee 1 + i32.eqz + br_if 1 (;@5;) + local.get 4 + i32.const 16 + i32.add + local.set 7 + end + loop ;; label = @6 + local.get 7 + local.set 9 + local.get 1 + local.tee 2 + i32.const 20 + i32.add + local.set 7 + local.get 2 + i32.load offset=20 + local.tee 1 + br_if 0 (;@6;) + local.get 2 + i32.const 16 + i32.add + local.set 7 + local.get 2 + i32.load offset=16 + local.tee 1 + br_if 0 (;@6;) + end + local.get 9 + i32.const 0 + i32.store + br 1 (;@4;) + end + i32.const 0 + local.set 2 + end + local.get 8 + i32.eqz + br_if 0 (;@3;) + block ;; label = @4 + block ;; label = @5 + local.get 4 + local.get 4 + i32.load offset=28 + local.tee 7 + i32.const 2 + i32.shl + i32.const 68104 + i32.add + local.tee 1 + i32.load + i32.ne + br_if 0 (;@5;) + local.get 1 + local.get 2 + i32.store + local.get 2 + br_if 1 (;@4;) + i32.const 0 + i32.const 0 + i32.load offset=67804 + i32.const -2 + local.get 7 + i32.rotl + i32.and + i32.store offset=67804 + br 2 (;@3;) + end + block ;; label = @5 + block ;; label = @6 + local.get 8 + i32.load offset=16 + local.get 4 + i32.ne + br_if 0 (;@6;) + local.get 8 + local.get 2 + i32.store offset=16 + br 1 (;@5;) + end + local.get 8 + local.get 2 + i32.store offset=20 + end + local.get 2 + i32.eqz + br_if 1 (;@3;) + end + local.get 2 + local.get 8 + i32.store offset=24 + block ;; label = @4 + local.get 4 + i32.load offset=16 + local.tee 1 + i32.eqz + br_if 0 (;@4;) + local.get 2 + local.get 1 + i32.store offset=16 + local.get 1 + local.get 2 + i32.store offset=24 + end + local.get 4 + i32.load offset=20 + local.tee 1 + i32.eqz + br_if 0 (;@3;) + local.get 2 + local.get 1 + i32.store offset=20 + local.get 1 + local.get 2 + i32.store offset=24 + end + local.get 6 + local.get 0 + i32.add + local.set 0 + local.get 4 + local.get 6 + i32.add + local.tee 4 + i32.load offset=4 + local.set 1 + end + local.get 4 + local.get 1 + i32.const -2 + i32.and + i32.store offset=4 + local.get 5 + local.get 0 + i32.const 1 + i32.or + i32.store offset=4 + local.get 5 + local.get 0 + i32.add + local.get 0 + i32.store + block ;; label = @2 + local.get 0 + i32.const 255 + i32.gt_u + br_if 0 (;@2;) + local.get 0 + i32.const -8 + i32.and + i32.const 67840 + i32.add + local.set 2 + block ;; label = @3 + block ;; label = @4 + i32.const 0 + i32.load offset=67800 + local.tee 1 + i32.const 1 + local.get 0 + i32.const 3 + i32.shr_u + i32.shl + local.tee 0 + i32.and + br_if 0 (;@4;) + i32.const 0 + local.get 1 + local.get 0 + i32.or + i32.store offset=67800 + local.get 2 + local.set 0 + br 1 (;@3;) + end + local.get 2 + i32.load offset=8 + local.set 0 + end + local.get 2 + local.get 5 + i32.store offset=8 + local.get 0 + local.get 5 + i32.store offset=12 + local.get 5 + local.get 2 + i32.store offset=12 + local.get 5 + local.get 0 + i32.store offset=8 + br 1 (;@1;) + end + i32.const 31 + local.set 2 + block ;; label = @2 + local.get 0 + i32.const 16777215 + i32.gt_u + br_if 0 (;@2;) + local.get 0 + i32.const 38 + local.get 0 + i32.const 8 + i32.shr_u + i32.clz + local.tee 2 + i32.sub + i32.shr_u + i32.const 1 + i32.and + local.get 2 + i32.const 1 + i32.shl + i32.sub + i32.const 62 + i32.add + local.set 2 + end + local.get 5 + local.get 2 + i32.store offset=28 + local.get 5 + i64.const 0 + i64.store offset=16 align=4 + local.get 2 + i32.const 2 + i32.shl + i32.const 68104 + i32.add + local.set 1 + block ;; label = @2 + block ;; label = @3 + block ;; label = @4 + i32.const 0 + i32.load offset=67804 + local.tee 7 + i32.const 1 + local.get 2 + i32.shl + local.tee 4 + i32.and + br_if 0 (;@4;) + i32.const 0 + local.get 7 + local.get 4 + i32.or + i32.store offset=67804 + local.get 1 + local.get 5 + i32.store + local.get 5 + local.get 1 + i32.store offset=24 + br 1 (;@3;) + end + local.get 0 + i32.const 0 + i32.const 25 + local.get 2 + i32.const 1 + i32.shr_u + i32.sub + local.get 2 + i32.const 31 + i32.eq + select + i32.shl + local.set 2 + local.get 1 + i32.load + local.set 7 + loop ;; label = @4 + local.get 7 + local.tee 1 + i32.load offset=4 + i32.const -8 + i32.and + local.get 0 + i32.eq + br_if 2 (;@2;) + local.get 2 + i32.const 29 + i32.shr_u + local.set 7 + local.get 2 + i32.const 1 + i32.shl + local.set 2 + local.get 1 + local.get 7 + i32.const 4 + i32.and + i32.add + local.tee 4 + i32.load offset=16 + local.tee 7 + br_if 0 (;@4;) + end + local.get 4 + i32.const 16 + i32.add + local.get 5 + i32.store + local.get 5 + local.get 1 + i32.store offset=24 + end + local.get 5 + local.get 5 + i32.store offset=12 + local.get 5 + local.get 5 + i32.store offset=8 + br 1 (;@1;) + end + local.get 1 + i32.load offset=8 + local.tee 2 + local.get 5 + i32.store offset=12 + local.get 1 + local.get 5 + i32.store offset=8 + local.get 5 + i32.const 0 + i32.store offset=24 + local.get 5 + local.get 1 + i32.store offset=12 + local.get 5 + local.get 2 + i32.store offset=8 + end + local.get 3 + i32.const 8 + i32.add) + (func (;6;) (type 3) (param i32) + (local i32 i32 i32 i32 i32 i32 i32 i32) + block ;; label = @1 + local.get 0 + i32.eqz + br_if 0 (;@1;) + local.get 0 + i32.const -8 + i32.add + local.tee 1 + local.get 0 + i32.const -4 + i32.add + i32.load + local.tee 2 + i32.const -8 + i32.and + local.tee 0 + i32.add + local.set 3 + block ;; label = @2 + local.get 2 + i32.const 1 + i32.and + br_if 0 (;@2;) + local.get 2 + i32.const 2 + i32.and + i32.eqz + br_if 1 (;@1;) + local.get 1 + local.get 1 + i32.load + local.tee 4 + i32.sub + local.tee 1 + i32.const 0 + i32.load offset=67816 + i32.lt_u + br_if 1 (;@1;) + local.get 4 + local.get 0 + i32.add + local.set 0 + block ;; label = @3 + block ;; label = @4 + block ;; label = @5 + block ;; label = @6 + local.get 1 + i32.const 0 + i32.load offset=67820 + i32.eq + br_if 0 (;@6;) + local.get 1 + i32.load offset=12 + local.set 2 + block ;; label = @7 + local.get 4 + i32.const 255 + i32.gt_u + br_if 0 (;@7;) + local.get 2 + local.get 1 + i32.load offset=8 + local.tee 5 + i32.ne + br_if 2 (;@5;) + i32.const 0 + i32.const 0 + i32.load offset=67800 + i32.const -2 + local.get 4 + i32.const 3 + i32.shr_u + i32.rotl + i32.and + i32.store offset=67800 + br 5 (;@2;) + end + local.get 1 + i32.load offset=24 + local.set 6 + block ;; label = @7 + local.get 2 + local.get 1 + i32.eq + br_if 0 (;@7;) + local.get 1 + i32.load offset=8 + local.tee 4 + local.get 2 + i32.store offset=12 + local.get 2 + local.get 4 + i32.store offset=8 + br 4 (;@3;) + end + block ;; label = @7 + block ;; label = @8 + local.get 1 + i32.load offset=20 + local.tee 4 + i32.eqz + br_if 0 (;@8;) + local.get 1 + i32.const 20 + i32.add + local.set 5 + br 1 (;@7;) + end + local.get 1 + i32.load offset=16 + local.tee 4 + i32.eqz + br_if 3 (;@4;) + local.get 1 + i32.const 16 + i32.add + local.set 5 + end + loop ;; label = @7 + local.get 5 + local.set 7 + local.get 4 + local.tee 2 + i32.const 20 + i32.add + local.set 5 + local.get 2 + i32.load offset=20 + local.tee 4 + br_if 0 (;@7;) + local.get 2 + i32.const 16 + i32.add + local.set 5 + local.get 2 + i32.load offset=16 + local.tee 4 + br_if 0 (;@7;) + end + local.get 7 + i32.const 0 + i32.store + br 3 (;@3;) + end + local.get 3 + i32.load offset=4 + local.tee 2 + i32.const 3 + i32.and + i32.const 3 + i32.ne + br_if 3 (;@2;) + i32.const 0 + local.get 0 + i32.store offset=67808 + local.get 3 + local.get 2 + i32.const -2 + i32.and + i32.store offset=4 + local.get 1 + local.get 0 + i32.const 1 + i32.or + i32.store offset=4 + local.get 3 + local.get 0 + i32.store + return + end + local.get 5 + local.get 2 + i32.store offset=12 + local.get 2 + local.get 5 + i32.store offset=8 + br 2 (;@2;) + end + i32.const 0 + local.set 2 + end + local.get 6 + i32.eqz + br_if 0 (;@2;) + block ;; label = @3 + block ;; label = @4 + local.get 1 + local.get 1 + i32.load offset=28 + local.tee 5 + i32.const 2 + i32.shl + i32.const 68104 + i32.add + local.tee 4 + i32.load + i32.ne + br_if 0 (;@4;) + local.get 4 + local.get 2 + i32.store + local.get 2 + br_if 1 (;@3;) + i32.const 0 + i32.const 0 + i32.load offset=67804 + i32.const -2 + local.get 5 + i32.rotl + i32.and + i32.store offset=67804 + br 2 (;@2;) + end + block ;; label = @4 + block ;; label = @5 + local.get 6 + i32.load offset=16 + local.get 1 + i32.ne + br_if 0 (;@5;) + local.get 6 + local.get 2 + i32.store offset=16 + br 1 (;@4;) + end + local.get 6 + local.get 2 + i32.store offset=20 + end + local.get 2 + i32.eqz + br_if 1 (;@2;) + end + local.get 2 + local.get 6 + i32.store offset=24 + block ;; label = @3 + local.get 1 + i32.load offset=16 + local.tee 4 + i32.eqz + br_if 0 (;@3;) + local.get 2 + local.get 4 + i32.store offset=16 + local.get 4 + local.get 2 + i32.store offset=24 + end + local.get 1 + i32.load offset=20 + local.tee 4 + i32.eqz + br_if 0 (;@2;) + local.get 2 + local.get 4 + i32.store offset=20 + local.get 4 + local.get 2 + i32.store offset=24 + end + local.get 1 + local.get 3 + i32.ge_u + br_if 0 (;@1;) + local.get 3 + i32.load offset=4 + local.tee 4 + i32.const 1 + i32.and + i32.eqz + br_if 0 (;@1;) + block ;; label = @2 + block ;; label = @3 + block ;; label = @4 + block ;; label = @5 + block ;; label = @6 + local.get 4 + i32.const 2 + i32.and + br_if 0 (;@6;) + block ;; label = @7 + local.get 3 + i32.const 0 + i32.load offset=67824 + i32.ne + br_if 0 (;@7;) + i32.const 0 + local.get 1 + i32.store offset=67824 + i32.const 0 + i32.const 0 + i32.load offset=67812 + local.get 0 + i32.add + local.tee 0 + i32.store offset=67812 + local.get 1 + local.get 0 + i32.const 1 + i32.or + i32.store offset=4 + local.get 1 + i32.const 0 + i32.load offset=67820 + i32.ne + br_if 6 (;@1;) + i32.const 0 + i32.const 0 + i32.store offset=67808 + i32.const 0 + i32.const 0 + i32.store offset=67820 + return + end + block ;; label = @7 + local.get 3 + i32.const 0 + i32.load offset=67820 + local.tee 6 + i32.ne + br_if 0 (;@7;) + i32.const 0 + local.get 1 + i32.store offset=67820 + i32.const 0 + i32.const 0 + i32.load offset=67808 + local.get 0 + i32.add + local.tee 0 + i32.store offset=67808 + local.get 1 + local.get 0 + i32.const 1 + i32.or + i32.store offset=4 + local.get 1 + local.get 0 + i32.add + local.get 0 + i32.store + return + end + local.get 4 + i32.const -8 + i32.and + local.get 0 + i32.add + local.set 0 + local.get 3 + i32.load offset=12 + local.set 2 + block ;; label = @7 + local.get 4 + i32.const 255 + i32.gt_u + br_if 0 (;@7;) + block ;; label = @8 + local.get 2 + local.get 3 + i32.load offset=8 + local.tee 5 + i32.ne + br_if 0 (;@8;) + i32.const 0 + i32.const 0 + i32.load offset=67800 + i32.const -2 + local.get 4 + i32.const 3 + i32.shr_u + i32.rotl + i32.and + i32.store offset=67800 + br 5 (;@3;) + end + local.get 5 + local.get 2 + i32.store offset=12 + local.get 2 + local.get 5 + i32.store offset=8 + br 4 (;@3;) + end + local.get 3 + i32.load offset=24 + local.set 8 + block ;; label = @7 + local.get 2 + local.get 3 + i32.eq + br_if 0 (;@7;) + local.get 3 + i32.load offset=8 + local.tee 4 + local.get 2 + i32.store offset=12 + local.get 2 + local.get 4 + i32.store offset=8 + br 3 (;@4;) + end + block ;; label = @7 + block ;; label = @8 + local.get 3 + i32.load offset=20 + local.tee 4 + i32.eqz + br_if 0 (;@8;) + local.get 3 + i32.const 20 + i32.add + local.set 5 + br 1 (;@7;) + end + local.get 3 + i32.load offset=16 + local.tee 4 + i32.eqz + br_if 2 (;@5;) + local.get 3 + i32.const 16 + i32.add + local.set 5 + end + loop ;; label = @7 + local.get 5 + local.set 7 + local.get 4 + local.tee 2 + i32.const 20 + i32.add + local.set 5 + local.get 2 + i32.load offset=20 + local.tee 4 + br_if 0 (;@7;) + local.get 2 + i32.const 16 + i32.add + local.set 5 + local.get 2 + i32.load offset=16 + local.tee 4 + br_if 0 (;@7;) + end + local.get 7 + i32.const 0 + i32.store + br 2 (;@4;) + end + local.get 3 + local.get 4 + i32.const -2 + i32.and + i32.store offset=4 + local.get 1 + local.get 0 + i32.const 1 + i32.or + i32.store offset=4 + local.get 1 + local.get 0 + i32.add + local.get 0 + i32.store + br 3 (;@2;) + end + i32.const 0 + local.set 2 + end + local.get 8 + i32.eqz + br_if 0 (;@3;) + block ;; label = @4 + block ;; label = @5 + local.get 3 + local.get 3 + i32.load offset=28 + local.tee 5 + i32.const 2 + i32.shl + i32.const 68104 + i32.add + local.tee 4 + i32.load + i32.ne + br_if 0 (;@5;) + local.get 4 + local.get 2 + i32.store + local.get 2 + br_if 1 (;@4;) + i32.const 0 + i32.const 0 + i32.load offset=67804 + i32.const -2 + local.get 5 + i32.rotl + i32.and + i32.store offset=67804 + br 2 (;@3;) + end + block ;; label = @5 + block ;; label = @6 + local.get 8 + i32.load offset=16 + local.get 3 + i32.ne + br_if 0 (;@6;) + local.get 8 + local.get 2 + i32.store offset=16 + br 1 (;@5;) + end + local.get 8 + local.get 2 + i32.store offset=20 + end + local.get 2 + i32.eqz + br_if 1 (;@3;) + end + local.get 2 + local.get 8 + i32.store offset=24 + block ;; label = @4 + local.get 3 + i32.load offset=16 + local.tee 4 + i32.eqz + br_if 0 (;@4;) + local.get 2 + local.get 4 + i32.store offset=16 + local.get 4 + local.get 2 + i32.store offset=24 + end + local.get 3 + i32.load offset=20 + local.tee 4 + i32.eqz + br_if 0 (;@3;) + local.get 2 + local.get 4 + i32.store offset=20 + local.get 4 + local.get 2 + i32.store offset=24 + end + local.get 1 + local.get 0 + i32.const 1 + i32.or + i32.store offset=4 + local.get 1 + local.get 0 + i32.add + local.get 0 + i32.store + local.get 1 + local.get 6 + i32.ne + br_if 0 (;@2;) + i32.const 0 + local.get 0 + i32.store offset=67808 + return + end + block ;; label = @2 + local.get 0 + i32.const 255 + i32.gt_u + br_if 0 (;@2;) + local.get 0 + i32.const -8 + i32.and + i32.const 67840 + i32.add + local.set 2 + block ;; label = @3 + block ;; label = @4 + i32.const 0 + i32.load offset=67800 + local.tee 4 + i32.const 1 + local.get 0 + i32.const 3 + i32.shr_u + i32.shl + local.tee 0 + i32.and + br_if 0 (;@4;) + i32.const 0 + local.get 4 + local.get 0 + i32.or + i32.store offset=67800 + local.get 2 + local.set 0 + br 1 (;@3;) + end + local.get 2 + i32.load offset=8 + local.set 0 + end + local.get 2 + local.get 1 + i32.store offset=8 + local.get 0 + local.get 1 + i32.store offset=12 + local.get 1 + local.get 2 + i32.store offset=12 + local.get 1 + local.get 0 + i32.store offset=8 + return + end + i32.const 31 + local.set 2 + block ;; label = @2 + local.get 0 + i32.const 16777215 + i32.gt_u + br_if 0 (;@2;) + local.get 0 + i32.const 38 + local.get 0 + i32.const 8 + i32.shr_u + i32.clz + local.tee 2 + i32.sub + i32.shr_u + i32.const 1 + i32.and + local.get 2 + i32.const 1 + i32.shl + i32.sub + i32.const 62 + i32.add + local.set 2 + end + local.get 1 + local.get 2 + i32.store offset=28 + local.get 1 + i64.const 0 + i64.store offset=16 align=4 + local.get 2 + i32.const 2 + i32.shl + i32.const 68104 + i32.add + local.set 5 + block ;; label = @2 + block ;; label = @3 + block ;; label = @4 + block ;; label = @5 + i32.const 0 + i32.load offset=67804 + local.tee 4 + i32.const 1 + local.get 2 + i32.shl + local.tee 3 + i32.and + br_if 0 (;@5;) + i32.const 0 + local.get 4 + local.get 3 + i32.or + i32.store offset=67804 + local.get 5 + local.get 1 + i32.store + i32.const 8 + local.set 0 + i32.const 24 + local.set 2 + br 1 (;@4;) + end + local.get 0 + i32.const 0 + i32.const 25 + local.get 2 + i32.const 1 + i32.shr_u + i32.sub + local.get 2 + i32.const 31 + i32.eq + select + i32.shl + local.set 2 + local.get 5 + i32.load + local.set 5 + loop ;; label = @5 + local.get 5 + local.tee 4 + i32.load offset=4 + i32.const -8 + i32.and + local.get 0 + i32.eq + br_if 2 (;@3;) + local.get 2 + i32.const 29 + i32.shr_u + local.set 5 + local.get 2 + i32.const 1 + i32.shl + local.set 2 + local.get 4 + local.get 5 + i32.const 4 + i32.and + i32.add + local.tee 3 + i32.load offset=16 + local.tee 5 + br_if 0 (;@5;) + end + local.get 3 + i32.const 16 + i32.add + local.get 1 + i32.store + i32.const 8 + local.set 0 + i32.const 24 + local.set 2 + local.get 4 + local.set 5 + end + local.get 1 + local.set 4 + local.get 1 + local.set 3 + br 1 (;@2;) + end + local.get 4 + i32.load offset=8 + local.tee 5 + local.get 1 + i32.store offset=12 + local.get 4 + local.get 1 + i32.store offset=8 + i32.const 0 + local.set 3 + i32.const 24 + local.set 0 + i32.const 8 + local.set 2 + end + local.get 1 + local.get 2 + i32.add + local.get 5 + i32.store + local.get 1 + local.get 4 + i32.store offset=12 + local.get 1 + local.get 0 + i32.add + local.get 3 + i32.store + i32.const 0 + i32.const 0 + i32.load offset=67832 + i32.const -1 + i32.add + local.tee 1 + i32.const -1 + local.get 1 + select + i32.store offset=67832 + end) + (func (;7;) (type 0) (result i32) + memory.size + i32.const 16 + i32.shl) + (func (;8;) (type 1) (param i32) (result i32) + i32.const 0) + (func (;9;) (type 1) (param i32) (result i32) + (local i32 i32) + i32.const 0 + i32.load offset=67764 + local.tee 1 + local.get 0 + i32.const 7 + i32.add + i32.const -8 + i32.and + local.tee 2 + i32.add + local.set 0 + block ;; label = @1 + block ;; label = @2 + block ;; label = @3 + local.get 2 + i32.eqz + br_if 0 (;@3;) + local.get 0 + local.get 1 + i32.le_u + br_if 1 (;@2;) + end + local.get 0 + call 7 + i32.le_u + br_if 1 (;@1;) + local.get 0 + call 8 + br_if 1 (;@1;) + end + call 3 + i32.const 48 + i32.store + i32.const -1 + return + end + i32.const 0 + local.get 0 + i32.store offset=67764 + local.get 1) + (func (;10;) (type 2) + i32.const 65536 + global.set 2 + i32.const 0 + i32.const 15 + i32.add + i32.const -16 + i32.and + global.set 1) + (func (;11;) (type 0) (result i32) + global.get 0 + global.get 1 + i32.sub) + (func (;12;) (type 0) (result i32) + global.get 2) + (func (;13;) (type 0) (result i32) + global.get 1) + (func (;14;) (type 3) (param i32) + local.get 0 + global.set 0) + (func (;15;) (type 1) (param i32) (result i32) + (local i32 i32) + global.get 0 + local.get 0 + i32.sub + i32.const -16 + i32.and + local.tee 1 + global.set 0 + local.get 1) + (func (;16;) (type 0) (result i32) + global.get 0) + (func (;17;) (type 6) (param i32 i32) (result i32) + i32.const 0 + local.get 0 + local.get 0 + i32.const 153 + i32.gt_u + select + i32.const 1 + i32.shl + i32.const 67456 + i32.add + i32.load16_u + i32.const 65536 + i32.add) + (func (;18;) (type 1) (param i32) (result i32) + local.get 0 + local.get 0 + call 17) + (table (;0;) 2 2 funcref) + (memory (;0;) 258 258) + (global (;0;) (mut i32) (i32.const 65536)) + (global (;1;) (mut i32) (i32.const 0)) + (global (;2;) (mut i32) (i32.const 0)) + (export "memory" (memory 0)) + (export "initSize" (func 1)) + (export "malloc" (func 4)) + (export "__indirect_function_table" (table 0)) + (export "_initialize" (func 2)) + (export "strerror" (func 18)) + (export "free" (func 6)) + (export "emscripten_stack_init" (func 10)) + (export "emscripten_stack_get_free" (func 11)) + (export "emscripten_stack_get_base" (func 12)) + (export "emscripten_stack_get_end" (func 13)) + (export "_emscripten_stack_restore" (func 14)) + (export "_emscripten_stack_alloc" (func 15)) + (export "emscripten_stack_get_current" (func 16)) + (elem (;0;) (i32.const 1) func 0) + (data (;0;) (i32.const 65536) "No error information\00Illegal byte sequence\00Domain error\00Result not representable\00Not a tty\00Permission denied\00Operation not permitted\00No such file or directory\00No such process\00File exists\00Value too large for data type\00No space left on device\00Out of memory\00Resource busy\00Interrupted system call\00Resource temporarily unavailable\00Invalid seek\00Cross-device link\00Read-only file system\00Directory not empty\00Connection reset by peer\00Operation timed out\00Connection refused\00Host is down\00Host is unreachable\00Address in use\00Broken pipe\00I/O error\00No such device or address\00Block device required\00No such device\00Not a directory\00Is a directory\00Text file busy\00Exec format error\00Invalid argument\00Argument list too long\00Symbolic link loop\00Filename too long\00Too many open files in system\00No file descriptors available\00Bad file descriptor\00No child process\00Bad address\00File too large\00Too many links\00No locks available\00Resource deadlock would occur\00State not recoverable\00Previous owner died\00Operation canceled\00Function not implemented\00No message of desired type\00Identifier removed\00Device not a stream\00No data available\00Device timeout\00Out of streams resources\00Link has been severed\00Protocol error\00Bad message\00File descriptor in bad state\00Not a socket\00Destination address required\00Message too large\00Protocol wrong type for socket\00Protocol not available\00Protocol not supported\00Socket type not supported\00Not supported\00Protocol family not supported\00Address family not supported by protocol\00Address not available\00Network is down\00Network unreachable\00Connection reset by network\00Connection aborted\00No buffer space available\00Socket is connected\00Socket not connected\00Cannot send after socket shutdown\00Operation already in progress\00Operation in progress\00Stale file handle\00Remote I/O error\00Quota exceeded\00No medium found\00Wrong medium type\00Multihop attempted\00Required key not available\00Key has expired\00Key has been revoked\00Key was rejected by service\00\00\00\00\00\00\00\00\00\a5\02[\00\f0\01\b5\05\8c\05%\01\83\06\1d\03\94\04\ff\00\c7\031\03\0b\06\bc\01\8f\01\7f\03\ca\04+\00\da\06\af\00B\03N\03\dc\01\0e\04\15\00\a1\06\0d\01\94\02\0b\028\06d\02\bc\02\ff\02]\03\e7\04\0b\07\cf\02\cb\05\ef\05\db\05\e1\02\1e\06E\02\85\00\82\02l\03o\04\f1\00\f3\03\18\05\d9\00\da\03L\06T\02{\01\9d\03\bd\04\00\00Q\00\15\02\bb\00\b3\03m\00\ff\01\85\04/\05\f9\048\00e\01F\01\9f\00\b7\06\a8\01s\02S\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00!\04\00\00\00\00\00\00\00\00/\02\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\005\04G\04V\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\a0\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00F\05`\05n\05a\06\00\00\cf\01\00\00\00\00\00\00\00\00\c9\06\e9\06\f9\06\1e\079\07I\07^\07") + (data (;1;) (i32.const 67764) "\d0\0a\01\00")) diff --git a/package-lock.json b/package-lock.json index 654034bf..8fe2810e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,15 +10,18 @@ "dependencies": { "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", + "@react-spring/web": "^9.5.5", "@types/react-modal": "^3.16.3", - "framer-motion": "^11.11.17", + "framer-motion": "^11.18.2", "prettier-plugin-emotion-order": "^1.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-horizontal-scrolling-menu": "^8.2.0", "react-query": "^3.39.3", "react-router-dom": "^6.27.0", + "react-tinder-card": "^1.6.4", "recoil": "^0.7.7", + "uuid": "^11.1.0", "vite-plugin-radar": "^0.9.6" }, "devDependencies": { @@ -29,6 +32,7 @@ "@types/node": "^22.10.1", "@types/react": "^18.3.10", "@types/react-dom": "^18.3.0", + "@types/recharts": "^2.0.1", "@typescript-eslint/eslint-plugin": "^8.12.2", "@typescript-eslint/parser": "^8.12.2", "@vite-pwa/assets-generator": "^0.2.6", @@ -108,6 +112,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -1810,6 +1815,7 @@ "version": "11.13.3", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz", "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.12.0", @@ -2486,6 +2492,101 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@react-spring/animated": { + "version": "9.5.5", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.5.5.tgz", + "integrity": "sha512-glzViz7syQ3CE6BQOwAyr75cgh0qsihm5lkaf24I0DfU63cMm/3+br299UEYkuaHNmfDfM414uktiPlZCNJbQA==", + "dependencies": { + "@react-spring/shared": "~9.5.5", + "@react-spring/types": "~9.5.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.5.5", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.5.5.tgz", + "integrity": "sha512-shaJYb3iX18Au6gkk8ahaF0qx0LpS0Yd+ajb4asBaAQf6WPGuEdJsbsNSgei1/O13JyEATsJl20lkjeslJPMYA==", + "dependencies": { + "@react-spring/animated": "~9.5.5", + "@react-spring/rafz": "~9.5.5", + "@react-spring/shared": "~9.5.5", + "@react-spring/types": "~9.5.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.5.5", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.5.5.tgz", + "integrity": "sha512-F/CLwB0d10jL6My5vgzRQxCNY2RNyDJZedRBK7FsngdCmzoq3V4OqqNc/9voJb9qRC2wd55oGXUeXv2eIaFmsw==" + }, + "node_modules/@react-spring/shared": { + "version": "9.5.5", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.5.5.tgz", + "integrity": "sha512-YwW70Pa/YXPOwTutExHZmMQSHcNC90kJOnNR4G4mCDNV99hE98jWkIPDOsgqbYx3amIglcFPiYKMaQuGdr8dyQ==", + "dependencies": { + "@react-spring/rafz": "~9.5.5", + "@react-spring/types": "~9.5.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.5.5", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.5.5.tgz", + "integrity": "sha512-7I/qY8H7Enwasxr4jU6WmtNK+RZ4Z/XvSlDvjXFVe7ii1x0MoSlkw6pD7xuac8qrHQRm9BTcbZNyeeKApYsvCg==" + }, + "node_modules/@react-spring/web": { + "version": "9.5.5", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.5.5.tgz", + "integrity": "sha512-+moT8aDX/ho/XAhU+HRY9m0LVV9y9CK6NjSRaI+30Re150pB3iEip6QfnF4qnhSCQ5drpMF0XRXHgOTY/xbtFw==", + "peer": true, + "dependencies": { + "@react-spring/animated": "~9.5.5", + "@react-spring/core": "~9.5.5", + "@react-spring/shared": "~9.5.5", + "@react-spring/types": "~9.5.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", + "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@remix-run/router": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", @@ -2760,6 +2861,20 @@ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "dev": true, + "license": "MIT" + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -2931,6 +3046,7 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -3394,6 +3510,78 @@ "node": ">=10.13.0" } }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/emscripten": { "version": "1.39.13", "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.13.tgz", @@ -3418,6 +3606,7 @@ "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.20.0" } @@ -3459,6 +3648,17 @@ "@types/react": "*" } }, + "node_modules/@types/recharts": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/recharts/-/recharts-2.0.1.tgz", + "integrity": "sha512-/cFs7oiafzByUwBSWA1IzE6FW+ppPwQAWsDTadSgVOwzveY9MESpyLHyyHY0SfPPKLW4+4qVNYHPXd0rFiC8vg==", + "deprecated": "This is a stub types definition. recharts provides its own type definitions, so you do not need this installed.", + "dev": true, + "license": "MIT", + "dependencies": { + "recharts": "*" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -3471,6 +3671,13 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "dev": true }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.12.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.12.2.tgz", @@ -3509,6 +3716,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.12.2.tgz", "integrity": "sha512-MrvlXNfGPLH3Z+r7Tk+Z5moZAc0dzdVjTgUgwsdGweH7lydysQsnSww3nAmsq8blFuRD5VRlAr9YdEFw3e6PBw==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.12.2", "@typescript-eslint/types": "8.12.2", @@ -3676,6 +3884,7 @@ "resolved": "https://registry.npmjs.org/@vite-pwa/assets-generator/-/assets-generator-0.2.6.tgz", "integrity": "sha512-kK44dXltvoubEo5B+6tCGjUrOWOE1+dA4DForbFpO1rKy2wSkAVGrs8tyfN6DzTig89/QKyV8XYodgmaKyrYng==", "dev": true, + "peer": true, "dependencies": { "cac": "^6.7.14", "colorette": "^2.0.20", @@ -3711,6 +3920,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", "devOptional": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4212,6 +4422,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", @@ -4304,9 +4515,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001677", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001677.tgz", - "integrity": "sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==", + "version": "1.0.30001724", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001724.tgz", + "integrity": "sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA==", "dev": true, "funding": [ { @@ -4360,6 +4571,16 @@ "node": ">=12" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -4645,6 +4866,138 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dev": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -4712,6 +5065,13 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "dev": true, + "license": "MIT" + }, "node_modules/decode-bmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/decode-bmp/-/decode-bmp-0.2.1.tgz", @@ -5131,6 +5491,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.39.8", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.8.tgz", + "integrity": "sha512-A8QO9TfF+rltS8BXpdu8OS+rpGgEdnRhqIVxO/ZmNvnXBYgOdSsxukT55ELyP94gZIntWJ+Li9QRrT2u1Kitpg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -5194,6 +5565,7 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5249,6 +5621,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -5651,6 +6024,13 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -5843,17 +6223,18 @@ } }, "node_modules/framer-motion": { - "version": "11.11.17", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.17.tgz", - "integrity": "sha512-O8QzvoKiuzI5HSAHbcYuL6xU+ZLXbrH7C8Akaato4JzQbX2ULNeniqC2Vo5eiCtFktX9XsJ+7nUhxcl2E2IjpA==", - "license": "MIT", + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/is-prop-valid": { @@ -6246,6 +6627,17 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -6305,6 +6697,16 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -7114,6 +7516,19 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7385,6 +7800,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-sleep": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-sleep/-/p-sleep-1.1.0.tgz", + "integrity": "sha512-bwP3GKZirBUYMtiUuBrheLUQdRXVeE/pmHOaLpNJzNfAD4b5AjDn6l823brXcQFade4G/g7GMNQ3KV86E8EaEw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7463,6 +7883,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -7498,6 +7919,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", @@ -7631,6 +8053,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7875,6 +8298,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -7886,6 +8310,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -7930,7 +8355,8 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "peer": true }, "node_modules/react-query": { "version": "3.39.3", @@ -7957,6 +8383,31 @@ } } }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "6.27.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", @@ -7987,6 +8438,27 @@ "react-dom": ">=16.8" } }, + "node_modules/react-tinder-card": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/react-tinder-card/-/react-tinder-card-1.6.4.tgz", + "integrity": "sha512-IC6YXoBZ+51jm7XsT8i+8G/ov8rvAob+kBRdp9unQyjsLc7jmuYb1cNfu95Q3mdFDgwE0AzTIyl1o2Klm61+aQ==", + "dependencies": { + "p-sleep": "^1.1.0" + }, + "peerDependencies": { + "@react-spring/native": "^9.5.5", + "@react-spring/web": "^9.5.5", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@react-spring/native": { + "optional": true + }, + "@react-spring/web": { + "optional": true + } + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -8001,6 +8473,34 @@ "node": ">= 6" } }, + "node_modules/recharts": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.1.0.tgz", + "integrity": "sha512-NqAqQcGBmLrfDs2mHX/bz8jJCQtG2FeXfE0GqpZmIuXIjkpIwj8sd9ad0WyvKiBKPd8ZgNG0hL85c8sFDwascw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/recoil": { "version": "0.7.7", "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", @@ -8020,6 +8520,24 @@ } } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -8150,6 +8668,13 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -8226,6 +8751,7 @@ "version": "4.24.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "peer": true, "dependencies": { "@types/estree": "1.0.6" }, @@ -8985,6 +9511,13 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", @@ -9200,6 +9733,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9269,6 +9803,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.11.0.tgz", "integrity": "sha512-lmt73NeHdy1Q/2ul295Qy3uninSqi6wQI18XwSpm8w0ZbQXUpjCAWP1Vlv/obudoBiIjJVjlztjQ+d/Md98Yxg==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.11.0", "@typescript-eslint/types": "8.11.0", @@ -9580,16 +10115,63 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "dev": true, + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "5.4.11", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -9981,6 +10563,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -10033,6 +10616,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, diff --git a/package.json b/package.json index bb425bc0..278c00b9 100644 --- a/package.json +++ b/package.json @@ -21,15 +21,18 @@ "dependencies": { "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", + "@react-spring/web": "^9.5.5", "@types/react-modal": "^3.16.3", - "framer-motion": "^11.11.17", + "framer-motion": "^11.18.2", "prettier-plugin-emotion-order": "^1.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-horizontal-scrolling-menu": "^8.2.0", "react-query": "^3.39.3", "react-router-dom": "^6.27.0", + "react-tinder-card": "^1.6.4", "recoil": "^0.7.7", + "uuid": "^11.1.0", "vite-plugin-radar": "^0.9.6" }, "devDependencies": { @@ -40,6 +43,7 @@ "@types/node": "^22.10.1", "@types/react": "^18.3.10", "@types/react-dom": "^18.3.0", + "@types/recharts": "^2.0.1", "@typescript-eslint/eslint-plugin": "^8.12.2", "@typescript-eslint/parser": "^8.12.2", "@vite-pwa/assets-generator": "^0.2.6", diff --git a/src/components/PWAUsage/Android/Android.style.tsx b/public/404.html similarity index 100% rename from src/components/PWAUsage/Android/Android.style.tsx rename to public/404.html diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 00000000..8446b831 --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,26 @@ +{ + "name": "인간지표 : 주식투자심리도우미", + "short_name": "인간지표", + "start_url": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#3457FD", + "icons": [ + { + "src": "/pwa-64x64.png", + "sizes": "64x64", + "type": "image/png" + }, + { + "src": "/pwa-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/pwa-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] +} diff --git a/src/App.css b/src/App.css index cd3aafc8..1730b6bc 100644 --- a/src/App.css +++ b/src/App.css @@ -1,3 +1,7 @@ body { font-family: 'Pretendard', sans-serif; } + +input { + font-family: 'Pretendard', sans-serif; +} diff --git a/src/assets/Loading.webm b/src/assets/Loading.webm new file mode 100644 index 00000000..8194fe2a Binary files /dev/null and b/src/assets/Loading.webm differ diff --git a/src/assets/PWA/Android/AddToHome.png b/src/assets/PWA/Android/AddToHome.png new file mode 100644 index 00000000..cdf9b788 Binary files /dev/null and b/src/assets/PWA/Android/AddToHome.png differ diff --git a/src/assets/PWA/Android/AddToHome.svg b/src/assets/PWA/Android/AddToHome.svg deleted file mode 100644 index 3dd12d75..00000000 --- a/src/assets/PWA/Android/AddToHome.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/PWA/Android/ShareButton.png b/src/assets/PWA/Android/ShareButton.png new file mode 100644 index 00000000..c3f6b8d3 Binary files /dev/null and b/src/assets/PWA/Android/ShareButton.png differ diff --git a/src/assets/PWA/Android/ShareButton.svg b/src/assets/PWA/Android/ShareButton.svg deleted file mode 100644 index 2435b303..00000000 --- a/src/assets/PWA/Android/ShareButton.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/PWA/IOS/AddToHome.png b/src/assets/PWA/IOS/AddToHome.png new file mode 100644 index 00000000..8dcb8968 Binary files /dev/null and b/src/assets/PWA/IOS/AddToHome.png differ diff --git a/src/assets/PWA/IOS/AddToHome.svg b/src/assets/PWA/IOS/AddToHome.svg deleted file mode 100644 index 3e952afb..00000000 --- a/src/assets/PWA/IOS/AddToHome.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/PWA/IOS/ShareButton.png b/src/assets/PWA/IOS/ShareButton.png new file mode 100644 index 00000000..8cf81d54 Binary files /dev/null and b/src/assets/PWA/IOS/ShareButton.png differ diff --git a/src/assets/PWA/IOS/ShareButton.svg b/src/assets/PWA/IOS/ShareButton.svg deleted file mode 100644 index 4b9b3e30..00000000 --- a/src/assets/PWA/IOS/ShareButton.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/assets/PWA/RunApp.png b/src/assets/PWA/RunApp.png new file mode 100644 index 00000000..555c40fd Binary files /dev/null and b/src/assets/PWA/RunApp.png differ diff --git a/src/assets/PWA/RunApp.svg b/src/assets/PWA/RunApp.svg deleted file mode 100644 index 20990c54..00000000 --- a/src/assets/PWA/RunApp.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/assets/README.md b/src/assets/README.md new file mode 100644 index 00000000..1f2ebd65 --- /dev/null +++ b/src/assets/README.md @@ -0,0 +1,4 @@ +https://brunch.co.kr/@sunking1126/4 + +1. snake_case +2. {icon name}_{ciecle}_{fill}.svg diff --git a/src/assets/alarm.svg b/src/assets/alarm.svg new file mode 100644 index 00000000..64996995 --- /dev/null +++ b/src/assets/alarm.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/appleLogin.png b/src/assets/appleLogin.png new file mode 100644 index 00000000..c6f10ba0 Binary files /dev/null and b/src/assets/appleLogin.png differ diff --git a/src/assets/arrowLeft.svg b/src/assets/arrowLeft.svg new file mode 100644 index 00000000..4ac59567 --- /dev/null +++ b/src/assets/arrowLeft.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/backLogo.svg b/src/assets/backLogo.svg new file mode 100644 index 00000000..a0506264 --- /dev/null +++ b/src/assets/backLogo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/background.svg b/src/assets/background.svg new file mode 100644 index 00000000..21927bd3 --- /dev/null +++ b/src/assets/background.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/blueAlert.svg b/src/assets/blueAlert.svg new file mode 100644 index 00000000..914866ed --- /dev/null +++ b/src/assets/blueAlert.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/bottomNav/favorites.svg b/src/assets/bottomNav/favorites.svg new file mode 100644 index 00000000..181523c3 --- /dev/null +++ b/src/assets/bottomNav/favorites.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/bottomNav/home.svg b/src/assets/bottomNav/home.svg new file mode 100644 index 00000000..5a7ede81 --- /dev/null +++ b/src/assets/bottomNav/home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/bottomNav/lab.svg b/src/assets/bottomNav/lab.svg new file mode 100644 index 00000000..e2a8d844 --- /dev/null +++ b/src/assets/bottomNav/lab.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/bottomNav/myPage.svg b/src/assets/bottomNav/myPage.svg new file mode 100644 index 00000000..1fb42afc --- /dev/null +++ b/src/assets/bottomNav/myPage.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/bottomNav/shortView.svg b/src/assets/bottomNav/shortView.svg new file mode 100644 index 00000000..b6e74885 --- /dev/null +++ b/src/assets/bottomNav/shortView.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/check.svg b/src/assets/check.svg new file mode 100644 index 00000000..df5410d3 --- /dev/null +++ b/src/assets/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/checkCircle.svg b/src/assets/checkCircle.svg new file mode 100644 index 00000000..45bcfc57 --- /dev/null +++ b/src/assets/checkCircle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/close.svg b/src/assets/close.svg new file mode 100644 index 00000000..96bb7707 --- /dev/null +++ b/src/assets/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/default-stock-image.png b/src/assets/default-stock-image.png new file mode 100644 index 00000000..9aafbd07 Binary files /dev/null and b/src/assets/default-stock-image.png differ diff --git a/src/assets/design/alarmExample.png b/src/assets/design/alarmExample.png new file mode 100644 index 00000000..b13194a3 Binary files /dev/null and b/src/assets/design/alarmExample.png differ diff --git a/src/assets/design/antVoice.png b/src/assets/design/antVoice.png new file mode 100644 index 00000000..20dbab33 Binary files /dev/null and b/src/assets/design/antVoice.png differ diff --git a/src/assets/design/callout/callout_tail.svg b/src/assets/design/callout/callout_tail.svg new file mode 100644 index 00000000..0169b370 --- /dev/null +++ b/src/assets/design/callout/callout_tail.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/design/card/card.svg b/src/assets/design/card/card.svg new file mode 100644 index 00000000..20a9e429 --- /dev/null +++ b/src/assets/design/card/card.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/design/common_rule.png b/src/assets/design/common_rule.png new file mode 100644 index 00000000..d83b517d Binary files /dev/null and b/src/assets/design/common_rule.png differ diff --git a/src/assets/design/defaultAlarm.png b/src/assets/design/defaultAlarm.png new file mode 100644 index 00000000..a6742710 Binary files /dev/null and b/src/assets/design/defaultAlarm.png differ diff --git a/src/assets/design/defaultAlarm2.png b/src/assets/design/defaultAlarm2.png new file mode 100644 index 00000000..35aed438 Binary files /dev/null and b/src/assets/design/defaultAlarm2.png differ diff --git a/src/assets/design/defaultStock.png b/src/assets/design/defaultStock.png new file mode 100644 index 00000000..9aafbd07 Binary files /dev/null and b/src/assets/design/defaultStock.png differ diff --git a/src/assets/edit_circle.svg b/src/assets/edit_circle.svg new file mode 100644 index 00000000..3ac4ae06 --- /dev/null +++ b/src/assets/edit_circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/flags/korea.png b/src/assets/flags/korea.png new file mode 100644 index 00000000..09fa7f58 Binary files /dev/null and b/src/assets/flags/korea.png differ diff --git a/src/assets/flags/oversea.png b/src/assets/flags/oversea.png new file mode 100644 index 00000000..aaab8c2a Binary files /dev/null and b/src/assets/flags/oversea.png differ diff --git a/src/assets/footer/footer_term.svg b/src/assets/footer/footer_term.svg new file mode 100644 index 00000000..cf2b8ff0 --- /dev/null +++ b/src/assets/footer/footer_term.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/googleLogin.png b/src/assets/googleLogin.png new file mode 100644 index 00000000..58ad90bd Binary files /dev/null and b/src/assets/googleLogin.png differ diff --git a/src/assets/heart.svg b/src/assets/heart.svg new file mode 100644 index 00000000..c027e60e --- /dev/null +++ b/src/assets/heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/addStock.svg b/src/assets/icons/addStock.svg new file mode 100644 index 00000000..d78cfef1 --- /dev/null +++ b/src/assets/icons/addStock.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/alarm.svg b/src/assets/icons/alarm.svg new file mode 100644 index 00000000..983f1d76 --- /dev/null +++ b/src/assets/icons/alarm.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/alert.svg b/src/assets/icons/alert.svg new file mode 100644 index 00000000..19b55863 --- /dev/null +++ b/src/assets/icons/alert.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/arrowDropUp.svg b/src/assets/icons/arrowDropUp.svg new file mode 100644 index 00000000..67d0872f --- /dev/null +++ b/src/assets/icons/arrowDropUp.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/arrowLeft.svg b/src/assets/icons/arrowLeft.svg new file mode 100644 index 00000000..2cb22ff6 --- /dev/null +++ b/src/assets/icons/arrowLeft.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/arrowUp.svg b/src/assets/icons/arrowUp.svg new file mode 100644 index 00000000..a100acd3 --- /dev/null +++ b/src/assets/icons/arrowUp.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/bell.svg b/src/assets/icons/bell.svg new file mode 100644 index 00000000..05dd2b5f --- /dev/null +++ b/src/assets/icons/bell.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/icons/cancel.svg b/src/assets/icons/cancel.svg index bfbe9125..45e9f74b 100644 --- a/src/assets/icons/cancel.svg +++ b/src/assets/icons/cancel.svg @@ -1,5 +1,5 @@ - - + + diff --git a/src/assets/icons/check.svg b/src/assets/icons/check.svg new file mode 100644 index 00000000..47b3969d --- /dev/null +++ b/src/assets/icons/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/checkCircle.svg b/src/assets/icons/checkCircle.svg new file mode 100644 index 00000000..76045ba7 --- /dev/null +++ b/src/assets/icons/checkCircle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/chevronDown.svg b/src/assets/icons/chevronDown.svg new file mode 100644 index 00000000..31657796 --- /dev/null +++ b/src/assets/icons/chevronDown.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/chevronLeft.svg b/src/assets/icons/chevronLeft.svg new file mode 100644 index 00000000..c3089976 --- /dev/null +++ b/src/assets/icons/chevronLeft.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/chevronLeftNarrow.svg b/src/assets/icons/chevronLeftNarrow.svg new file mode 100644 index 00000000..a2849a04 --- /dev/null +++ b/src/assets/icons/chevronLeftNarrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/chevronUp.svg b/src/assets/icons/chevronUp.svg new file mode 100644 index 00000000..63abaf3f --- /dev/null +++ b/src/assets/icons/chevronUp.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/clock.svg b/src/assets/icons/clock.svg new file mode 100644 index 00000000..88591a7b --- /dev/null +++ b/src/assets/icons/clock.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/cross.svg b/src/assets/icons/cross.svg new file mode 100644 index 00000000..e50546dd --- /dev/null +++ b/src/assets/icons/cross.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/detail.svg b/src/assets/icons/detail.svg new file mode 100644 index 00000000..05993bd8 --- /dev/null +++ b/src/assets/icons/detail.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/down.svg b/src/assets/icons/down.svg index 019f2e62..e8de41aa 100644 --- a/src/assets/icons/down.svg +++ b/src/assets/icons/down.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/assets/icons/edit.svg b/src/assets/icons/edit.svg new file mode 100644 index 00000000..86fb5251 --- /dev/null +++ b/src/assets/icons/edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/exclamation_mark_circle.svg b/src/assets/icons/exclamation_mark_circle.svg new file mode 100644 index 00000000..629af33f --- /dev/null +++ b/src/assets/icons/exclamation_mark_circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/heart.svg b/src/assets/icons/heart.svg new file mode 100644 index 00000000..f91993d0 --- /dev/null +++ b/src/assets/icons/heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/info.svg b/src/assets/icons/info.svg new file mode 100644 index 00000000..71ea6ac8 --- /dev/null +++ b/src/assets/icons/info.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/assets/icons/magnifier.svg b/src/assets/icons/magnifier.svg new file mode 100644 index 00000000..12359eb7 --- /dev/null +++ b/src/assets/icons/magnifier.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/money.svg b/src/assets/icons/money.svg new file mode 100644 index 00000000..8b243e87 --- /dev/null +++ b/src/assets/icons/money.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/pencil.svg b/src/assets/icons/pencil.svg new file mode 100644 index 00000000..f87925fc --- /dev/null +++ b/src/assets/icons/pencil.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/plus.svg b/src/assets/icons/plus.svg new file mode 100644 index 00000000..410fc02d --- /dev/null +++ b/src/assets/icons/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/purchaseCheck.svg b/src/assets/icons/purchaseCheck.svg new file mode 100644 index 00000000..076dce43 --- /dev/null +++ b/src/assets/icons/purchaseCheck.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/questionMark.svg b/src/assets/icons/questionMark.svg new file mode 100644 index 00000000..ebf452a4 --- /dev/null +++ b/src/assets/icons/questionMark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/question_mark_circle.svg b/src/assets/icons/question_mark_circle.svg new file mode 100644 index 00000000..b769fc90 --- /dev/null +++ b/src/assets/icons/question_mark_circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/rightArrow.svg b/src/assets/icons/rightArrow.svg index 175fda2a..95bbea30 100644 --- a/src/assets/icons/rightArrow.svg +++ b/src/assets/icons/rightArrow.svg @@ -1,3 +1,3 @@ - + diff --git a/src/assets/icons/search.svg b/src/assets/icons/search.svg index 3573a187..b803a26b 100644 --- a/src/assets/icons/search.svg +++ b/src/assets/icons/search.svg @@ -1,4 +1,3 @@ - - - + + diff --git a/src/assets/icons/shortview/chevronRight.svg b/src/assets/icons/shortview/chevronRight.svg new file mode 100644 index 00000000..01b40332 --- /dev/null +++ b/src/assets/icons/shortview/chevronRight.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/toast/bell.svg b/src/assets/icons/toast/bell.svg new file mode 100644 index 00000000..8be04040 --- /dev/null +++ b/src/assets/icons/toast/bell.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/toast/bell_cross.svg b/src/assets/icons/toast/bell_cross.svg new file mode 100644 index 00000000..ab7b4f1b --- /dev/null +++ b/src/assets/icons/toast/bell_cross.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/toast/heart.svg b/src/assets/icons/toast/heart.svg new file mode 100644 index 00000000..c8665314 --- /dev/null +++ b/src/assets/icons/toast/heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/uncheck.svg b/src/assets/icons/uncheck.svg new file mode 100644 index 00000000..581378f9 --- /dev/null +++ b/src/assets/icons/uncheck.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/warning.svg b/src/assets/icons/warning.svg new file mode 100644 index 00000000..97ea2ae4 --- /dev/null +++ b/src/assets/icons/warning.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/whiteQuestionMark.svg b/src/assets/icons/whiteQuestionMark.svg new file mode 100644 index 00000000..a770f2ca --- /dev/null +++ b/src/assets/icons/whiteQuestionMark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/instagram.svg b/src/assets/instagram.svg new file mode 100644 index 00000000..4b5bb04e --- /dev/null +++ b/src/assets/instagram.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/kakaoLogin.png b/src/assets/kakaoLogin.png new file mode 100644 index 00000000..5994fef3 Binary files /dev/null and b/src/assets/kakaoLogin.png differ diff --git a/src/assets/koreaFlag.svg b/src/assets/koreaFlag.svg new file mode 100644 index 00000000..41942f9a --- /dev/null +++ b/src/assets/koreaFlag.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/lab.svg b/src/assets/lab.svg new file mode 100644 index 00000000..2fd0014b --- /dev/null +++ b/src/assets/lab.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/lab/checkCircleSelected.svg b/src/assets/lab/checkCircleSelected.svg new file mode 100644 index 00000000..d94e9453 --- /dev/null +++ b/src/assets/lab/checkCircleSelected.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/lab/checkCircleUnelected.svg b/src/assets/lab/checkCircleUnelected.svg new file mode 100644 index 00000000..75ed618c --- /dev/null +++ b/src/assets/lab/checkCircleUnelected.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/lab/lab-intro-1.png b/src/assets/lab/lab-intro-1.png new file mode 100644 index 00000000..f008df8e Binary files /dev/null and b/src/assets/lab/lab-intro-1.png differ diff --git a/src/assets/lab/lab-intro-2.png b/src/assets/lab/lab-intro-2.png new file mode 100644 index 00000000..01358f96 Binary files /dev/null and b/src/assets/lab/lab-intro-2.png differ diff --git a/src/assets/lab/lab-intro-3.png b/src/assets/lab/lab-intro-3.png new file mode 100644 index 00000000..11ac08b4 Binary files /dev/null and b/src/assets/lab/lab-intro-3.png differ diff --git a/src/assets/lab/lab-result.png b/src/assets/lab/lab-result.png new file mode 100644 index 00000000..c7862c81 Binary files /dev/null and b/src/assets/lab/lab-result.png differ diff --git a/src/assets/lab/labTutorial1.png b/src/assets/lab/labTutorial1.png new file mode 100644 index 00000000..f008df8e Binary files /dev/null and b/src/assets/lab/labTutorial1.png differ diff --git a/src/assets/lab/labTutorial2.png b/src/assets/lab/labTutorial2.png new file mode 100644 index 00000000..01358f96 Binary files /dev/null and b/src/assets/lab/labTutorial2.png differ diff --git a/src/assets/lab/labTutorial3.png b/src/assets/lab/labTutorial3.png new file mode 100644 index 00000000..11ac08b4 Binary files /dev/null and b/src/assets/lab/labTutorial3.png differ diff --git a/src/assets/lab/no-result.svg b/src/assets/lab/no-result.svg new file mode 100644 index 00000000..7178354a --- /dev/null +++ b/src/assets/lab/no-result.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/assets/linkedin.svg b/src/assets/linkedin.svg new file mode 100644 index 00000000..3ddb34b5 --- /dev/null +++ b/src/assets/linkedin.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/loading.png b/src/assets/loading.png new file mode 100644 index 00000000..97b215cf Binary files /dev/null and b/src/assets/loading.png differ diff --git a/src/assets/login/apple.svg b/src/assets/login/apple.svg new file mode 100644 index 00000000..c0d22e95 --- /dev/null +++ b/src/assets/login/apple.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/login/google.svg b/src/assets/login/google.svg new file mode 100644 index 00000000..d97aa4af --- /dev/null +++ b/src/assets/login/google.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/login/kakao.svg b/src/assets/login/kakao.svg new file mode 100644 index 00000000..5505b8ad --- /dev/null +++ b/src/assets/login/kakao.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/assets/login/naver.svg b/src/assets/login/naver.svg new file mode 100644 index 00000000..2261a406 --- /dev/null +++ b/src/assets/login/naver.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/logo/full_logo_white.svg b/src/assets/logo/full_logo_white.svg new file mode 100644 index 00000000..0629ce8a --- /dev/null +++ b/src/assets/logo/full_logo_white.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/logo/logo_white.svg b/src/assets/logo/logo_white.svg new file mode 100644 index 00000000..541daed6 --- /dev/null +++ b/src/assets/logo/logo_white.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/logo/slime.svg b/src/assets/logo/slime.svg new file mode 100644 index 00000000..f3ab11ad --- /dev/null +++ b/src/assets/logo/slime.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/logo_with_title_white.svg b/src/assets/logo_with_title_white.svg new file mode 100644 index 00000000..a3254260 --- /dev/null +++ b/src/assets/logo_with_title_white.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/mask_balloon.png b/src/assets/mask_balloon.png new file mode 100644 index 00000000..7cccad95 Binary files /dev/null and b/src/assets/mask_balloon.png differ diff --git a/src/assets/naverLogin.png b/src/assets/naverLogin.png new file mode 100644 index 00000000..06ab3d8c Binary files /dev/null and b/src/assets/naverLogin.png differ diff --git a/src/assets/noFavorites.png b/src/assets/noFavorites.png new file mode 100644 index 00000000..bbf8e884 Binary files /dev/null and b/src/assets/noFavorites.png differ diff --git a/src/assets/profile.png b/src/assets/profile.png new file mode 100644 index 00000000..23c6d2a3 Binary files /dev/null and b/src/assets/profile.png differ diff --git a/src/assets/right_arrow_thick.svg b/src/assets/right_arrow_thick.svg new file mode 100644 index 00000000..3d6004a2 --- /dev/null +++ b/src/assets/right_arrow_thick.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/shortView.svg b/src/assets/shortView.svg new file mode 100644 index 00000000..6b0b653f --- /dev/null +++ b/src/assets/shortView.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/short_view_mock.png b/src/assets/short_view_mock.png new file mode 100644 index 00000000..816455d7 Binary files /dev/null and b/src/assets/short_view_mock.png differ diff --git a/src/assets/stockScore/bad.png b/src/assets/stockScore/bad.png index 82914a88..a184b557 100644 Binary files a/src/assets/stockScore/bad.png and b/src/assets/stockScore/bad.png differ diff --git a/src/assets/stockScore/excellent.png b/src/assets/stockScore/excellent.png index 7466d6fd..98c31acb 100644 Binary files a/src/assets/stockScore/excellent.png and b/src/assets/stockScore/excellent.png differ diff --git a/src/assets/stockScore/good.png b/src/assets/stockScore/good.png index 75d183d3..7b0e7074 100644 Binary files a/src/assets/stockScore/good.png and b/src/assets/stockScore/good.png differ diff --git a/src/assets/stockScore/normal.png b/src/assets/stockScore/normal.png index e722f190..0e689490 100644 Binary files a/src/assets/stockScore/normal.png and b/src/assets/stockScore/normal.png differ diff --git a/src/assets/stockScore/poor.png b/src/assets/stockScore/poor.png index 93deaaf5..6d565fec 100644 Binary files a/src/assets/stockScore/poor.png and b/src/assets/stockScore/poor.png differ diff --git a/src/assets/swipe_hand.png b/src/assets/swipe_hand.png new file mode 100644 index 00000000..ede53f4f Binary files /dev/null and b/src/assets/swipe_hand.png differ diff --git a/src/assets/thread.svg b/src/assets/thread.svg new file mode 100644 index 00000000..5125db75 --- /dev/null +++ b/src/assets/thread.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/tmp_img.png b/src/assets/tmp_img.png new file mode 100644 index 00000000..2b4fbc03 Binary files /dev/null and b/src/assets/tmp_img.png differ diff --git a/src/assets/usFlag.svg b/src/assets/usFlag.svg new file mode 100644 index 00000000..6f88520a --- /dev/null +++ b/src/assets/usFlag.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/withdraw.png b/src/assets/withdraw.png new file mode 100644 index 00000000..9dc4c3ce Binary files /dev/null and b/src/assets/withdraw.png differ diff --git a/src/components/CardList/CardList.Style.ts b/src/components/CardList/CardList.Style.ts new file mode 100644 index 00000000..f46755d1 --- /dev/null +++ b/src/components/CardList/CardList.Style.ts @@ -0,0 +1,10 @@ +import styled from '@emotion/styled'; + +const CardListContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '12px', + width: '100%', +}); + +export { CardListContainer }; diff --git a/src/components/CardList/CardList.tsx b/src/components/CardList/CardList.tsx index df8cb1b0..f49a6a6d 100644 --- a/src/components/CardList/CardList.tsx +++ b/src/components/CardList/CardList.tsx @@ -1,50 +1,40 @@ -import { STOCK_COUNTRY } from '@ts/Types'; -import { useIsMobile } from '@hooks/useIsMobile'; -import { useQueryComponent } from '@hooks/useQueryComponent'; -import StockCard from '@components/CardList/StockCard/StockCard'; -import { StockType } from '@components/Common/Common.Type'; -import SlideView from '@components/SlideView/SlideView'; -import ScoreSlotMachine from '@components/StockSlotMachine/StockSlotMachine'; -import { StockInfo } from '@controllers/api.Type'; -import { useHomeStockFetchQuery } from '@controllers/query'; +import { STOCK_UPDATE_TIME } from '@ts/Constants'; +import { STOCK_COUNTRY_MAP, StockCountryKey } from '@ts/StockCountry'; +import useModal from '@hooks/useModal'; +import { HomeItemTtile } from '@components/Home/Title/Title.Style'; +import DescentPopUp from '@components/PopUp/DescentPopUp/DescentPopUp'; +import HotPopUp from '@components/PopUp/HotPopUp/HotPopUp'; +import RisingPopUp from '@components/PopUp/RisingPopUp/RisingPopUp'; +import InfoSVG from '@assets/icons/info.svg?react'; +import { CardListContainer } from './CardList.Style'; +import StockCard from './StockCard/StockCard'; -const CardList = ({ name, country }: { name: StockType; country: STOCK_COUNTRY }) => { - const isHot = name === 'HOT'; - const isMobile = useIsMobile(); - const [curStocks, suspend] = useQueryComponent({ query: useHomeStockFetchQuery(name, country) }); - - return ( - suspend || - (curStocks && ( - - )) - ); +type CardListType = 'HOT' | 'RISING' | 'DESCENT'; +const cardListTitle: Record = { + HOT: '가장 HOT 한', + RISING: '🔥지금 민심 떡상 중인', + DESCENT: '💧지금 민심 떡락 중인', }; -const StockRisingDescend = (curStocks: StockInfo[], country: STOCK_COUNTRY) => { - return curStocks.map((stock: StockInfo) => { - return ; +const CardList = ({ type, country }: { type: CardListType; country: StockCountryKey }) => { + const { Modal, openModal } = useModal({ + Component: type === 'HOT' ? HotPopUp : type === 'RISING' ? RisingPopUp : DescentPopUp, }); -}; -const StockHot = (curStocks: StockInfo[], country: STOCK_COUNTRY) => { - return curStocks.map((stock: StockInfo) => { - return ( - - ); - }); + const updateTime = STOCK_UPDATE_TIME[country]; + const title = `${cardListTitle[type]} ${type === 'HOT' ? `${STOCK_COUNTRY_MAP[country].text}지표` : ''}`; + + return ( + + +

{title}

+ +

어제 {updateTime} 기준

+ + + + + ); }; export default CardList; diff --git a/src/components/CardList/StockCard/StockCard.Style.ts b/src/components/CardList/StockCard/StockCard.Style.ts index ab51506f..efa74069 100644 --- a/src/components/CardList/StockCard/StockCard.Style.ts +++ b/src/components/CardList/StockCard/StockCard.Style.ts @@ -1,139 +1,230 @@ import styled from '@emotion/styled'; -import { media, theme, themeColor } from '@styles/themes'; +import { deltaScoreToColor } from '@utils/ScoreConvert'; +import { theme } from '@styles/themes'; -export const StockCardContainer = styled.div({ - background: theme.colors.grayscale100, - borderRadius: '12px', - padding: '24px 32px', +const StockCardContainer = styled.div({ display: 'flex', - flexDirection: 'column-reverse', - gap: '32px', - cursor: 'pointer', - lineHeight: 1, + overflow: 'auto', + scrollSnapType: 'x mandatory', + + transition: 'background-color 0.1s ease-in-out', + backgroundColor: theme.colors.sub_black, [':hover']: { - background: theme.colors.grayscale90, + backgroundColor: theme.colors.sub_gray5, + }, + + ['>div']: { + display: 'flex', + gap: '12px', + padding: '0px 20px 8px', + backgroundColor: theme.colors.sub_black, }, - [media[0]]: { - padding: '12px', - flexDirection: 'row', - gap: '18px', + ['::-webkit-scrollbar']: { + height: '6px', + }, + ['::-webkit-scrollbar-track']: { + background: theme.colors.sub_black, + }, + ['::-webkit-scrollbar-thumb']: { + background: 'inherit', + borderRadius: '4px', + }, + + ['@media (max-width: 768px)']: { + ['>div']: { + padding: '0px 20px', + }, + + msOverflowStyle: 'none', + ['::-webkit-scrollbar']: { + display: 'none', + }, }, }); -export const StockCardTitle = styled.div({ +const StockCardItem = styled.div({ + flexShrink: '0', + display: 'flex', + width: '300px', + scrollSnapAlign: 'center', + background: theme.colors.sub_gray11, + borderRadius: '8px', +}); + +// +const LargeStockCardContainer = styled.div({ + flexShrink: '0', + // display: 'block', display: 'flex', - overflow: 'hidden', - width: '100%', flexDirection: 'column', - color: theme.colors.primary0, - gap: '12px', + width: '100%', + scrollSnapAlign: 'center', + background: theme.colors.sub_gray11, + borderRadius: '8px', - [media[0]]: { - gap: '12px', - justifyContent: 'space-between', + ['>hr']: { + border: `2px solid ${theme.colors.sub_black}`, + margin: '0px', }, }); -export const StockCardTitleContents = styled.div({ +const LargeStockCardHeader = styled.div({ display: 'flex', - flexDirection: 'column', - padding: '12px 0', - gap: '18px', + gap: '8px', + alignItems: 'center', + padding: '12px 12px 8px', + overflow: 'hidden', - [media[0]]: { - padding: '8px 0', - gap: '12px', + ['>p']: { + ...theme.font.title20Semibold, + color: theme.colors.sub_gray3, + margin: '0px', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }, + + ['>img']: { + width: '24px', + height: '24px', + borderRadius: '50%', + flexShrink: '0', }, }); -export const StockCardTitleName = styled.span({ - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', - fontSize: '24px', - fontWeight: '700', +const LargeStockCardHeaderImage = styled.div({ + width: '24px', + height: '24px', + borderRadius: '50%', + background: 'red', + flexShrink: '0', +}); + +const LargeStockCardContent = styled.div({ + display: 'flex', + padding: '16px 12px 12px', + gap: '12px', - [media[0]]: { - fontSize: '15px', + ['>img']: { + width: '102px', + height: '92px', + borderRadius: '4px', }, }); -export const StockCardTitleScore = styled.div( - { +const LargeStockCardContentTextContainer = styled.div({ + display: 'grid', + gridTemplateColumns: 'repeat(2, 1fr)', + width: '100%', + + ['>div']: { display: 'flex', - fontSize: '32px', - fontWeight: '700', + flexDirection: 'column', alignItems: 'center', - gap: '8px', - - ['span']: { - fontSize: '18px', - display: 'flex', - alignItems: 'center', - gap: '4px', - ['svg']: { - height: '0.5em', - width: '0.5em', + justifyContent: 'center', + gap: '4px', + + ['>p']: { + margin: '0px', + + ['&.title']: { + ...theme.font.body14Medium, + color: theme.colors.sub_gray7, }, - }, - [media[0]]: { - fontSize: '21px', - ['span']: { - fontSize: '15px', + ['&.content']: { + ...theme.font.title20Semibold, + color: theme.colors.sub_gray5, }, }, }, - ({ diffColor }: { diffColor: themeColor }) => ({ - ['span']: { - color: theme.colors[diffColor], - ['svg']: { - fill: theme.colors[diffColor], - }, - }, - }), -); +}); -export const StockCardKeywords = styled.div({ +const SmallStockCardContainer = styled.div({ + flexShrink: '0', display: 'flex', - gap: '8px', + width: '100%', + scrollSnapAlign: 'center', + background: theme.colors.sub_gray11, + borderRadius: '8px', + padding: '12px', + gap: '16px', + boxSizing: 'border-box', + + ['>img']: { + width: '102px', + height: '92px', + borderRadius: '4px', + }, +}); - fontSize: '15px', +const SmallStockCardContent = styled.div({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + minWidth: '0px', +}); - ['span']: { - background: theme.colors.grayscale90, - padding: '4px 12px', - borderRadius: '32px', - }, - '::after': { - content: '""', - display: 'inline-block', - height: '1em', - padding: '4px 0', - }, +const SmallStockCardContentTitle = styled.div({ + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + width: '100%', - [media[0]]: { - fontSize: '13px', - ['span']: { - padding: '4px 8px', - }, + ['>p']: { + margin: '0px', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + ...theme.font.body18Semibold, + color: theme.colors.primary0, }, }); -export const StockCardImage = styled.div({ - height: '160px', - display: 'flex', - justifyContent: 'end', +const SmallStockCardContentScore = styled.div( + ({ delta, isNew }: { delta: number; isNew: boolean }) => ({ + ['>span']: { + color: isNew ? theme.colors.yellow : (deltaScoreToColor(delta) ?? theme.colors.sub_gray7), + }, + }), + { + ...theme.font.body18Semibold, + color: theme.colors.sub_gray4, - ['img']: { - height: '100%', - borderRadius: '12px', + display: 'flex', + gap: '4px', + alignItems: 'center', + + ['>span']: { + ...theme.font.body14Semibold, + }, }, +); + +const SmallStockCardContentKeywords = styled.div({ + display: 'flex', + gap: '10px', - [media[0]]: { - justifyContent: 'start', - height: '100px', + ['>p']: { + margin: '0px', + ...theme.font.body14Medium, + color: theme.colors.sub_gray6, }, }); + +export { + StockCardContainer, + StockCardItem, + LargeStockCardContainer, + LargeStockCardHeader, + LargeStockCardHeaderImage, + LargeStockCardContent, + LargeStockCardContentTextContainer, + SmallStockCardContainer, + SmallStockCardContent, + SmallStockCardContentTitle, + SmallStockCardContentScore, + SmallStockCardContentKeywords, +}; diff --git a/src/components/CardList/StockCard/StockCard.tsx b/src/components/CardList/StockCard/StockCard.tsx index 1e846741..a203c36d 100644 --- a/src/components/CardList/StockCard/StockCard.tsx +++ b/src/components/CardList/StockCard/StockCard.tsx @@ -1,56 +1,118 @@ import { useNavigate } from 'react-router-dom'; -import { STOCK_COUNTRY } from '@ts/Types'; -import { deltaColor } from '@utils/Delta'; -import { scoreToImage } from '@utils/ScoreConvert'; +import { StockCountryKey } from '@ts/StockCountry'; +import { STOCK_TYPE } from '@ts/Types'; +import { diffToValue, scoreToImage, scoreToText } from '@utils/ScoreConvert'; +import { useQueryComponent } from '@hooks/useQueryComponent'; import { webPath } from '@router/index'; -import { StockInfo } from '@controllers/api.Type'; -import DownSVG from '@assets/icons/down.svg?react'; -import UpSVG from '@assets/icons/up.svg?react'; +import StockImage from '@components/Common/StockImage'; +import { useHomeStockFetchQuery } from '@controllers/stocks/query'; +import { StockInfo } from '@controllers/stocks/types'; import { + LargeStockCardContainer, + LargeStockCardContent, + LargeStockCardContentTextContainer, + LargeStockCardHeader, + SmallStockCardContainer, + SmallStockCardContent, + SmallStockCardContentKeywords, + SmallStockCardContentScore, + SmallStockCardContentTitle, StockCardContainer, - StockCardImage, - StockCardKeywords, - StockCardTitle, - StockCardTitleContents, - StockCardTitleName, - StockCardTitleScore, + StockCardItem, } from './StockCard.Style'; -const signedNumber = (value: number) => { - const sign = value > 0 ? '+' : value < 0 ? '-' : ''; - return sign + Math.abs(value); +export const LargeStockCard = ({ + stock: { stockId, symbolName, score }, + country, +}: { + stock: StockInfo; + country: StockCountryKey; +}) => { + const navigate = useNavigate(); + + const handleClick = () => { + navigate(webPath.search(), { state: { symbolName: symbolName, country: country } }); + }; + + const scoreImage = scoreToImage(score); + const scoreText = scoreToText(score); + + return ( + + + +

{symbolName}

+
+
+ + + +
+

민심 키워드

+

{scoreText}

+
+
+

민심 점수

+

{score}점

+
+
+
+
+ ); }; -const StockCard = ({ stockInfo, country }: { stockInfo: StockInfo; country: STOCK_COUNTRY }) => { +export const SmallStockCard = ({ + stock: { stockId, symbolName, score, diff, keywords }, + country, +}: { + stock: StockInfo; + country: StockCountryKey; +}) => { const navigate = useNavigate(); - const { symbolName, score, diff, keywords } = stockInfo; - const deltaSVG = !diff ? ' -' : diff > 0 ? : ; - const scoreImage = scoreToImage(score); const handleClick = () => { navigate(webPath.search(), { state: { symbolName: symbolName, country: country } }); }; + const scoreImage = scoreToImage(score); + return ( - - - - - - - {symbolName} - - {score}점 - - {signedNumber(diff)}점{deltaSVG} - - - - - {keywords?.map((e, i) => {e})} - - - + + + + +

{symbolName}

+ + {score}점{score != diff ? `${diffToValue(diff)}점` : 'NEW!'} + +
+ + {keywords?.map((e) =>

#{e}

)} +
+
+
+ ); +}; + +const StockCard = ({ type, country }: { type: STOCK_TYPE; country: StockCountryKey }) => { + const [curStocks, suspend] = useQueryComponent({ query: useHomeStockFetchQuery(type, country) }); + + return ( + suspend || ( + +
+ {curStocks?.map((stock: StockInfo) => ( + + {type === 'HOT' ? ( + + ) : ( + + )} + + ))} +
+
+ ) ); }; diff --git a/src/components/Common/Button.ts b/src/components/Common/Button.ts new file mode 100644 index 00000000..6e7ed46d --- /dev/null +++ b/src/components/Common/Button.ts @@ -0,0 +1,17 @@ +import styled from '@emotion/styled'; +import { theme } from '@styles/themes'; + +const Button = styled.button({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: '8px', + background: theme.colors.sub_blue6, + border: 'none', + borderRadius: '8px', + padding: '10px 0px', + ...theme.font.body18Semibold, + color: theme.colors.sub_white, +}); + +export default Button; diff --git a/src/components/Common/Common.Type.ts b/src/components/Common/Common.Type.ts index 9884648a..457ced9f 100644 --- a/src/components/Common/Common.Type.ts +++ b/src/components/Common/Common.Type.ts @@ -4,7 +4,14 @@ export type SelfPosition = 'center' | 'end' | 'flex-end' | 'flex-start' | 'self- export type AlignItems = Globals | SelfPosition | 'baseline' | 'normal' | 'stretch' | (string & {}); export type ContentDistribution = 'space-around' | 'space-between' | 'space-evenly' | 'stretch'; export type ContentPosition = 'center' | 'end' | 'flex-end' | 'flex-start' | 'start'; -export type JustifyContent = Globals | ContentDistribution | ContentPosition | 'left' | 'normal' | 'right' | (string & {}); +export type JustifyContent = + | Globals + | ContentDistribution + | ContentPosition + | 'left' + | 'normal' + | 'right' + | (string & {}); export type FlexDirection = Globals | 'column' | 'column-reverse' | 'row' | 'row-reverse'; export type Width = | Globals @@ -33,5 +40,3 @@ export type Height = | 'min-content' | string; export type Padding = Globals | TLength | string; - -export type StockType = 'HOT' | 'RISING' | 'DESCENT'; diff --git a/src/components/Common/Common.tsx b/src/components/Common/Common.tsx index 4fdd9ee2..38ca9e2a 100644 --- a/src/components/Common/Common.tsx +++ b/src/components/Common/Common.tsx @@ -97,16 +97,20 @@ const StyledSVG = ({ const Container = styled.div({ display: 'flex', + flexDirection: 'column', boxSizing: 'border-box', width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', - borderRadius: '16px', - padding: '15%', + overflow: 'hidden', + + ['>img']: { + objectFit: 'contain', + objectPosition: 'center', - ['svg']: { width: '100%', + height: '100%', }, }); diff --git a/src/components/Common/ContentsItem.Style.ts b/src/components/Common/ContentsItem.Style.ts index 2b2ea7db..ea3add1f 100644 --- a/src/components/Common/ContentsItem.Style.ts +++ b/src/components/Common/ContentsItem.Style.ts @@ -1,3 +1,4 @@ +import { css } from '@emotion/react'; import styled from '@emotion/styled'; import { media, theme, themeColor } from '@styles/themes'; @@ -11,40 +12,37 @@ const ContentsItemContainer = styled.div({ }, }); -const ContentsItemTitle = styled.div(({ color }: { color?: themeColor }) => ({ - display: 'flex', - gap: '8px', - alignItems: 'center', - - color: theme.colors.grayscale10, - fontWeight: '700', - fontSize: '32px', - - ['.btn_info']: { - height: '0.8em', - marginLeft: '4px', - - cursor: 'pointer', - }, +const ContentsItemTitle = styled.div<{ color?: themeColor }>( + ({ color }) => + css({ + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + gap: '8px', + color: theme.colors.grayscale10, + ...theme.font.title20Semibold, - ['svg']: { - width: 'auto', - height: '0.9em', + ['.btn_info']: { + height: '0.8em', + marginLeft: '4px', + cursor: 'pointer', + }, - fill: color ? theme.colors[color] : '', - }, + ['svg']: { + width: 'auto', + height: '0.9em', + fill: color ? theme.colors[color] : '', + }, - [media[0]]: { - gap: '6px', - padding: '0 20px', + [media[0]]: { + gap: '6px', - fontSize: '24px', - - ['.btn_info']: { - marginLeft: '0px', - }, - }, -})); + ['.btn_info']: { + marginLeft: '0px', + }, + }, + }) +); const ContentsItemContent = styled.div({ display: 'flex', @@ -54,8 +52,25 @@ const ContentsItemContent = styled.div({ [media[0]]: { margin: '0 0px', - padding: '0 20px', }, }); + +export const DetailText = styled.div({ + ...theme.font.detail12Medium, + color: theme.colors.sub_gray6, +}); + +export const TitleDetailText = styled.div({ + ...theme.font.body14Medium, + color: theme.colors.sub_gray8, +}); + +export const ContentsItemTitleSeparator = styled.div({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + width: '100%', +}); + export { ContentsItemContainer, ContentsItemTitle, ContentsItemContent }; diff --git a/src/components/Common/Header.tsx b/src/components/Common/Header.tsx new file mode 100644 index 00000000..f13b475a --- /dev/null +++ b/src/components/Common/Header.tsx @@ -0,0 +1,72 @@ +import styled from '@emotion/styled'; +import { useNavigate } from 'react-router-dom'; +import { theme } from '@styles/themes'; +import ArrowLeftSVG from '@assets/arrowLeft.svg?react'; +import CloseSVG from '@assets/close.svg?react'; + +const HeaderContainer = styled.div({ + paddingBottom: '8px', + borderBottom: `4px solid ${theme.colors.sub_gray11}`, +}); + +const HeaderContents = styled.div({ + position: 'relative', + display: 'flex', + alignItems: 'center', + padding: '8px 20px', + gap: '12px', + + ['>p']: { + position: 'absolute', + left: '50%', + transform: 'translateX(-50%)', + ...theme.font.body18Semibold, + color: theme.colors.sub_white, + margin: '0', + }, + + ['>svg']: { + width: '32px', + height: 'auto', + aspectRatio: '1 / 1', + cursor: 'pointer', + fill: theme.colors.sub_gray5, + }, + + ['>span']: { + flexGrow: 1, + }, +}); + +const Header = ({ + title, + onBefore, + beforeIconType = 'back', +}: { + title: string; + onBefore?: () => void; + beforeIconType?: 'back' | 'close'; +}) => { + const navigate = useNavigate(); + + const handleBefore = () => { + if (onBefore) { + onBefore(); + } else { + navigate(-1); + } + }; + + return ( + + +

{title}

+ {beforeIconType === 'back' && } + + {beforeIconType === 'close' && } +
+
+ ); +}; + +export default Header; diff --git a/src/components/Common/ScrollTopButton/ScrollTopButton.tsx b/src/components/Common/ScrollTopButton/ScrollTopButton.tsx new file mode 100644 index 00000000..01c5d41b --- /dev/null +++ b/src/components/Common/ScrollTopButton/ScrollTopButton.tsx @@ -0,0 +1,58 @@ +import styled from '@emotion/styled'; +import { useEffect, useState } from 'react'; +import { theme } from '@styles/themes'; +import ArrowUpSVG from '@assets/icons/arrowUp.svg?react'; + +const ScrollTopButtonContainer = styled.div( + ({ isHidden }: { isHidden: boolean }) => ({ + display: isHidden ? 'none' : 'flex', + }), + { + position: 'fixed', + bottom: '96px', + right: '0', + width: '58px', + height: '58px', + margin: '20px', + borderRadius: '50%', + alignItems: 'center', + justifyContent: 'center', + background: 'rgba(52, 58, 64, 0.5)', + boxShadow: '5px 5px 10px rgba(0, 0, 0, 0.7)', + backdropFilter: 'blur(10px)', + + ['>svg']: { + width: '32px', + height: 'auto', + aspectRatio: '1 / 1', + fill: theme.colors.sub_white, + }, + }, +); + +const ScrollTopButton = () => { + const [isScrolled, setIsScrolled] = useState(false); + + const handleClickScrollTop = () => { + window.scrollTo({ + top: 0, + behavior: 'smooth', // 부드럽게 스크롤 + }); + }; + + useEffect(() => { + const handleScroll = () => { + setIsScrolled(window.scrollY > 0); + }; + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + return ( + + + + ); +}; + +export default ScrollTopButton; diff --git a/src/components/Common/StockImage.tsx b/src/components/Common/StockImage.tsx new file mode 100644 index 00000000..432f6906 --- /dev/null +++ b/src/components/Common/StockImage.tsx @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react'; +import { DEFAULT_STOCK_IMAGE, resolveStockImageUrl } from '@utils/stockImage'; + +interface StockImageProps { + stockId: number; + imageUrl?: string | null; + alt?: string; + className?: string; + style?: React.CSSProperties; + onError?: () => void; +} + +/** + * 주식 이미지 컴포넌트 + * 이미지 로딩 실패 시 자동으로 기본 이미지로 fallback + */ +const StockImage = ({ + stockId, + imageUrl, + alt = 'Stock image', + className, + style, + onError: customOnError, +}: StockImageProps) => { + const [imageError, setImageError] = useState(false); + const [imageSrc, setImageSrc] = useState(resolveStockImageUrl(stockId, imageUrl)); + + // stockId나 imageUrl이 변경되면 이미지 URL 재설정 + useEffect(() => { + const newUrl = resolveStockImageUrl(stockId, imageUrl); + setImageSrc(newUrl); + setImageError(false); + }, [stockId, imageUrl]); + + // 이미지 로딩 실패 시 기본 이미지로 변경 + const handleImageError = () => { + if (!imageError) { + setImageError(true); + setImageSrc(DEFAULT_STOCK_IMAGE); + customOnError?.(); + } + }; + + return {alt}; +}; + +export default StockImage; diff --git a/src/components/Home/Banner/Banner.Style.ts b/src/components/Home/Banner/Banner.Style.ts new file mode 100644 index 00000000..91598532 --- /dev/null +++ b/src/components/Home/Banner/Banner.Style.ts @@ -0,0 +1,115 @@ +// ad +import styled from '@emotion/styled'; +import { theme } from '@styles/themes'; + +const HomeAdContainer = styled.div({ + display: 'flex', + overflow: 'auto', + width: '100%', + height: '240px', + scrollSnapType: 'x mandatory', + + msOverflowStyle: 'none', + ['::-webkit-scrollbar']: { + display: 'none', + }, +}); + +const HomeAdItem = styled.div( + ({ backgroundColor }: { backgroundColor: string }) => ({ + background: backgroundColor, + }), + { + position: 'relative', + flexShrink: '0', + display: 'flex', + flexDirection: 'column', + width: '100%', + height: '100%', + padding: '24px 24px 18px', + boxSizing: 'border-box', + justifyContent: 'space-between', + scrollSnapAlign: 'start', + + ['>span']: { + position: 'absolute', + + [':nth-of-type(1)']: { + bottom: '0px', + left: '0px', + width: '100%', + height: '57px', + background: `rgba(255, 255, 255, 0.05)`, + }, + + [':nth-of-type(2)']: { + top: '0px', + right: '0px', + height: '100%', + width: '57px', + background: `rgba(255, 255, 255, 0.05)`, + }, + + [':nth-of-type(3)']: { + top: '0px', + right: '0px', + height: '100%', + width: '188.5px', + boxSizing: 'border-box', + borderStyle: 'solid', + borderWidth: '0px 0px 240px 131.5px', + borderColor: 'transparent transparent rgba(255, 255, 255, 0.05) transparent ', + }, + }, + }, +); + +const HomeAdItemContent = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '12px', + alignItems: 'flex-start', +}); + +const HomeAdItemTitle = styled.p({ + ...theme.font.heading24Semibold, + color: theme.colors.sub_white, + margin: '0px', + whiteSpace: 'pre', +}); + +const HomeAdItemDescription = styled.p({ + ...theme.font.title20Medium, + color: theme.colors.sub_white, + margin: '0px', +}); + +const HomeAdItemButton = styled.button({ + ...theme.font.body14Medium, + color: theme.colors.sub_white, + borderRadius: '999px', + padding: '6px 12px 6px 12px', + background: 'rgba(255, 255, 255, 0.12)', + border: '1px solid rgba(255, 255, 255, 0.1)', +}); + +const HomeAdItemIndex = styled.p({ + ...theme.font.body16Medium, + color: theme.colors.sub_gray5, + margin: '0px', + + ['>b']: { + ...theme.font.body16Semibold, + color: theme.colors.sub_white, + }, +}); + +export { + HomeAdContainer, + HomeAdItem, + HomeAdItemContent, + HomeAdItemTitle, + HomeAdItemDescription, + HomeAdItemButton, + HomeAdItemIndex, +}; diff --git a/src/components/Home/Banner/Banner.tsx b/src/components/Home/Banner/Banner.tsx new file mode 100644 index 00000000..ab76b762 --- /dev/null +++ b/src/components/Home/Banner/Banner.tsx @@ -0,0 +1,151 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { webPath } from '@router/index'; +import { theme } from '@styles/themes'; +import { + HomeAdContainer, + HomeAdItem, + HomeAdItemButton, + HomeAdItemContent, + HomeAdItemDescription, + HomeAdItemIndex, + HomeAdItemTitle, +} from './Banner.Style'; + +const Banner = () => { + const navigate = useNavigate(); + + const handleClickSNS = () => { + window.open('https://www.instagram.com/humanzipyo/'); + }; + + const handleClickServiceCenter = () => { + window.open('https://forms.gle/eus2xRNHGxbSBaAK9'); + }; + + const handleClickServiceGuide = () => { + navigate(webPath.about()); + }; + + const banners = [ + { + title: '인간지표 앱 출시', + sub: '보다 더 편리하게 사용해보세요', + button: { + text: '인간지표 SNS', + onClick: handleClickSNS, + }, + background: theme.colors.sub_blue6, + }, + { + title: '인간지표, 더 좋아질 수\n있게 도와주세요', + sub: '', + button: { + text: '불편사항 접수', + onClick: handleClickServiceCenter, + }, + background: theme.colors.sub_blue5, + }, + { + title: '인간지표는 어떻게\n활용할 수 있나요?', + sub: '', + button: { + text: '서비스 가이드', + onClick: handleClickServiceGuide, + }, + background: theme.colors.sub_gray9, + }, + ]; + + const containerRef = useRef(null); + const [currentIndex, setCurrentIndex] = useState(0); + const isAutoScrolling = useRef(false); // 자동 스크롤 중인지 구분 + const intervalRef = useRef(null); // 타이머 ref 추가 + + // 타이머 시작 함수 + const startTimer = useCallback(() => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + intervalRef.current = setInterval(() => { + setCurrentIndex((prev) => (prev + 1) % banners.length); + }, 3000); + }, [banners.length]); + // 컴포넌트 마운트 시 타이머 시작 + useEffect(() => { + startTimer(); + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, [startTimer]); + + // 인덱스 변경 시 스크롤 이동 + useEffect(() => { + if (containerRef.current) { + isAutoScrolling.current = true; + const containerWidth = containerRef.current.offsetWidth; + containerRef.current.scrollTo({ + left: containerWidth * currentIndex, + behavior: 'smooth', + }); + + setTimeout(() => { + isAutoScrolling.current = false; + }, 500); + } + }, [currentIndex]); + + // 사용자 수동 스크롤 시 인덱스 업데이트 및 타이머 리셋 + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + let scrollTimeout: NodeJS.Timeout; + + const handleScroll = () => { + if (isAutoScrolling.current) return; + + clearTimeout(scrollTimeout); + scrollTimeout = setTimeout(() => { + const containerWidth = container.offsetWidth; + const scrollLeft = container.scrollLeft; + const newIndex = Math.round(scrollLeft / containerWidth); + + if (newIndex >= 0 && newIndex < banners.length) { + setCurrentIndex(newIndex); + startTimer(); // 타이머 리셋! + } + }, 100); + }; + container.addEventListener('scroll', handleScroll); + return () => { + container.removeEventListener('scroll', handleScroll); + clearTimeout(scrollTimeout); + }; + }, [banners.length, startTimer]); // startTimer 의존성 추가 + + return ( + + {banners.map((e, idx, arr) => ( + + +
+ {e.title} + {e.sub} +
+ {e.button.text} → +
+ + {idx + 1} / {arr.length} + + + + +
+ ))} +
+ ); +}; +export default Banner; diff --git a/src/components/Home/IndexScore/HomeInfo.Style.ts b/src/components/Home/IndexScore/HomeInfo.Style.ts new file mode 100644 index 00000000..5cb6e9ac --- /dev/null +++ b/src/components/Home/IndexScore/HomeInfo.Style.ts @@ -0,0 +1,124 @@ +import styled from '@emotion/styled'; +import { deltaScoreToColor } from '@utils/ScoreConvert'; +import { theme } from '@styles/themes'; + +const HomeInfoContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '10px', + padding: '0 20px', +}); + +const HomeInfoScoreContainer = styled.div({ + display: 'flex', + gap: '10px', +}); + +const HomeInfoScoreItemContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + padding: '10px 12px', + background: theme.colors.sub_gray11, + borderRadius: '4px', + width: '100%', +}); + +const HomeInfoScoreItemHeader = styled.div({ + display: 'flex', + alignItems: 'center', + gap: '4px', + + ['>p']: { + margin: '0', + ...theme.font.body16Semibold, + color: theme.colors.sub_gray1, + whiteSpace: 'nowrap', + }, + + ['>svg']: { + width: '16px', + height: '16px', + fill: theme.colors.sub_gray1, + cursor: 'pointer', + }, +}); + +const HomeInfoScoreItemValue = styled.div( + ({ delta }: { delta: number }) => ({ + ['>p']: { + color: deltaScoreToColor(delta) ?? theme.colors.sub_gray1, + }, + + ['>svg']: { + fill: deltaScoreToColor(delta) ?? theme.colors.sub_gray1, + }, + }), + { + display: 'flex', + alignItems: 'center', + gap: '4px', + + ['>p']: { + margin: '0', + ...theme.font.title20Semibold, + whiteSpace: 'nowrap', + }, + + ['>svg']: { + width: '10px', + height: 'auto', + aspectRatio: '1 / 1', + }, + }, +); + +const HomeInfoBannerContainer = styled.div({ + display: 'flex', + alignItems: 'center', + padding: '12px 14px', + background: theme.colors.sub_gray11, + borderRadius: '4px', + gap: '16px', + cursor: 'pointer', + + ['>svg']: { + width: '24px', + height: 'auto', + aspectRatio: '1 / 1', + fill: theme.colors.sub_gray1, + padding: '12px', + background: theme.colors.sub_gray10, + borderRadius: '999px', + flexShrink: '0', + }, + + ['>div']: { + display: 'flex', + flexDirection: 'column', + gap: '2px', + + ['>p']: { + margin: '0', + + ['&.title']: { + ...theme.font.body16Semibold, + color: theme.colors.sub_gray3, + }, + + ['&.description']: { + ...theme.font.body14Medium, + color: theme.colors.sub_gray6, + whiteSpace: 'nowrap', + }, + }, + }, +}); + +export { + HomeInfoContainer, + HomeInfoScoreContainer, + HomeInfoScoreItemContainer, + HomeInfoScoreItemHeader, + HomeInfoScoreItemValue, + HomeInfoBannerContainer, +}; diff --git a/src/components/Home/IndexScore/HomeInfo.tsx b/src/components/Home/IndexScore/HomeInfo.tsx new file mode 100644 index 00000000..d198faa5 --- /dev/null +++ b/src/components/Home/IndexScore/HomeInfo.tsx @@ -0,0 +1,106 @@ +import { useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { StockCountryKey } from '@ts/StockCountry'; +import useModal from '@hooks/useModal'; +import { webPath } from '@router/index'; +import FearPopUp from '@components/PopUp/FearPopUp/FearPopUp'; +import { useIndexScoreQuery } from '@controllers/score/query'; +import DictSVG from '@assets/footer/footer_dict.svg?react'; +import DownSVG from '@assets/icons/down.svg?react'; +import ExclamationMarkSVG from '@assets/icons/exclamation_mark_circle.svg?react'; +import UpSVG from '@assets/icons/up.svg?react'; +import { + HomeInfoBannerContainer, + HomeInfoContainer, + HomeInfoScoreContainer, + HomeInfoScoreItemContainer, + HomeInfoScoreItemHeader, + HomeInfoScoreItemValue, +} from './HomeInfo.Style'; + +type ScoreIndex = 'kospiVix' | 'kospiIndex' | 'kosdaqIndex' | 'snpVix' | 'snpIndex' | 'nasdaqIndex'; + +const scoreIndexNames: Record = { + kospiVix: '공포지수', + kospiIndex: '코스피', + kosdaqIndex: '코스닥', + snpVix: '공포지수', + snpIndex: 'S&P 500', + nasdaqIndex: '나스닥', +}; + +const scoreIndexCountry: Record = { + KOREA: ['kospiVix', 'kospiIndex', 'kosdaqIndex'], + OVERSEA: ['snpVix', 'snpIndex', 'nasdaqIndex'], +}; + +const HomeInfo = ({ country }: { country: StockCountryKey }) => { + const navigate = useNavigate(); + + const { data: indexScore } = useIndexScoreQuery(); + + const transformed: Record = useMemo( + () => + Object.entries(indexScore ?? []).reduce( + (acc, [key, value], i, arr) => { + if (i % 2) return acc; + return { + ...acc, + [key]: { + name: scoreIndexNames[key as ScoreIndex], + value: value, + diff: arr[i + 1][1], + }, + }; + }, + {} as Record, + ), + [indexScore], + ); + + const handleBannerClick = () => { + navigate(webPath.about()); + }; + + const { Modal, openModal } = useModal({ + Component: FearPopUp, + }); + + if (!indexScore) return null; + + return ( + + + + {scoreIndexCountry[country].map((e, idx) => { + const { name, value, diff } = transformed[e] ?? {}; + + return ( + + +

{name}

+ {idx === 0 && } +
+ +

{value}

+ {diff === 0 ? '' : diff > 0 ? : } +
+
+ ); + })} +
+ + +
+

도대체 인간지표가 뭐지?

+

+ 내가 사면 떨어지고, + 내가 팔면 오르는 마법?? +

+
+
+
+ ); +}; + +export default HomeInfo; diff --git a/src/components/Home/IndexScore/IndexScore.style.ts b/src/components/Home/IndexScore/IndexScore.style.ts deleted file mode 100644 index 0741e146..00000000 --- a/src/components/Home/IndexScore/IndexScore.style.ts +++ /dev/null @@ -1,83 +0,0 @@ -import styled from '@emotion/styled'; -import { deltaScoreToColor } from '@utils/ScoreConvert'; -import { media, theme } from '@styles/themes'; - -const IndicesContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - gap: '8px', - alignItems: 'center', - justifyContent: 'center', - - whiteSpace: 'nowrap', -}); - -const IndexItem = styled.div({ - display: 'flex', - flex: 1, - gap: '4px', - justifyContent: 'space-between', - boxSizing: 'border-box', - padding: '18px 24px', - - color: theme.colors.primary0, - fontFamily: 'Pretendard', - fontStyle: 'normal', - lineHeight: '1.5', - - background: theme.colors.grayscale100, - borderRadius: '8px', - - [media[0]]: { - flexDirection: 'column', - gap: '8px', - alignItems: 'flex-start', - justifyContent: 'center', - padding: '12px', - }, -}); - -const IndexInfoContainer = styled.div({ - display: 'flex', - gap: '4px', - alignItems: 'center', - - fontWeight: '500', - fontSize: '18px', - - [media[0]]: { - fontWeight: '700', - fontSize: '11px', - }, - - ['svg']: { - width: 'auto', - height: '1.25em', - }, -}); - -const IndexDeltaScore = styled.div(({ delta }: { delta: number }) => ({ - display: 'flex', - gap: '4px', - alignItems: 'center', - - color: deltaScoreToColor(delta), - fontWeight: '700', - fontSize: '32px', - lineHeight: '1', - - [media[0]]: { - margin: '0 4px', - - fontSize: '24px', - }, - - ['svg']: { - width: 'auto', - height: '0.5em', - - fill: deltaScoreToColor(delta), - }, -})); - -export { IndicesContainer, IndexInfoContainer, IndexItem, IndexDeltaScore }; diff --git a/src/components/Home/IndexScore/IndexScore.tsx b/src/components/Home/IndexScore/IndexScore.tsx deleted file mode 100644 index f4fbee11..00000000 --- a/src/components/Home/IndexScore/IndexScore.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { useState } from 'react'; -import { useQueryComponent } from '@hooks/useQueryComponent'; -import FearPopUp from '@components/PopUp/FearPopUp/FearPopUp'; -import { useIndexScoreQuery } from '@controllers/query'; -import DownSVG from '@assets/icons/down.svg?react'; -import UpSVG from '@assets/icons/up.svg?react'; -import InfoSVG from '@assets/info.svg?react'; -import { IndexDeltaScore, IndexInfoContainer, IndexItem, IndicesContainer } from './IndexScore.style'; - -const stockIndices = [ - ['공포탐욕지수 ', '코스피', '코스닥'], - ['공포탐욕지수 ', 'S&P 500', '나스닥'], -]; - -const IndexScore = ({ tabIndex }: { tabIndex: number }) => { - const [indexScores, suspend] = useQueryComponent({ query: useIndexScoreQuery() }); - - const [isPopupOpen, setPopupOpen] = useState(false); - const togglePopup = () => setPopupOpen((prev) => !prev); - - const entries = Object.entries(indexScores ?? []); - - const transformed = entries.reduce<{ score: number; delta: number }[]>((acc, _, i) => { - if (i % 2 === 0) { - acc.push({ - score: entries[i][1] as number, - delta: entries[i + 1]?.[1] as number, - }); - } - return acc; - }, []); - - const splitIndex = transformed.length / 2; - const result = tabIndex === 0 ? transformed.slice(0, splitIndex) : transformed.slice(splitIndex); - - if (suspend) return null; - - return ( - - {result.map(({ score, delta }, idx) => ( - - - {stockIndices[tabIndex][idx]} - {idx === 0 && } - - - {Math.abs(score)}점 {delta === 0 ? '-' : delta > 0 ? : } - - - ))} - {isPopupOpen && } - - ); -}; - -export default IndexScore; diff --git a/src/components/Home/Keywords/Keywords.style.tsx b/src/components/Home/Keywords/Keywords.style.tsx index cc07c48e..90c96ff7 100644 --- a/src/components/Home/Keywords/Keywords.style.tsx +++ b/src/components/Home/Keywords/Keywords.style.tsx @@ -1,106 +1,36 @@ import styled from '@emotion/styled'; -import { media, theme } from '@styles/themes'; +import { theme } from '@styles/themes'; const KeywordsContainer = styled.div({ display: 'flex', flexDirection: 'column', - gap: '20px', + gap: '16px', }); -const TitleWrapper = styled.div({ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - - fontFamily: 'Pretendard', - whiteSpace: 'nowrap', - - ['> span']: { - color: theme.colors.grayscale60, - fontWeight: '500', - fontSize: '15px', - }, - - [media[0]]: { - padding: '0 20px', - - ['> span']: { - fontSize: '11px', - }, - }, -}); - -const Title = styled.div({ - display: 'flex', - alignItems: 'center', - - color: theme.colors.grayscale10, - fontWeight: 700, - fontSize: '32px', - fontStyle: 'normal', - lineHeight: '150%', - - ['svg']: { - width: '24px', - height: '24px', - marginLeft: '8px', - }, - - [media[0]]: { - fontSize: '16px', - - ['svg']: { - width: '20px', - }, - }, -}); - -const KeywordList = styled.div({ - overflow: 'auto', - - whiteSpace: 'nowrap', - - msOverflowStyle: 'none', - - ['::-webkit-scrollbar']: { - display: 'none', - }, - - [media[0]]: { - padding: '0 20px', - }, -}); - -const KeywordItemConainer = styled.div({ - display: 'flex', - gap: '12px', - alignItems: 'center', - justifyContent: 'center', - width: '100%', - - [media[0]]: { - gap: '8px', - justifyContent: 'start', - }, +const KeywordsGrid = styled.div({ + display: 'grid', + gridTemplateColumns: 'repeat(3, 1fr)', + gridTemplateRows: 'repeat(3, 1fr)', + rowGap: '12px', + columnGap: '8px', + padding: '0 20px', }); const KeywordItem = styled.div({ - padding: '8px 24px', - - color: theme.colors.primary0, - fontWeight: 700, - fontSize: '19px', - fontFamily: 'Pretendard', - textAlign: 'right', - - backgroundColor: theme.colors.grayscale100, - borderRadius: '30px', - - [media[0]]: { - padding: '8px 12px', - - fontSize: '13px', + minWidth: '0', + overflow: 'hidden', + padding: '12px', + backgroundColor: theme.colors.sub_gray11, + borderRadius: '8px', + + ['>p']: { + margin: '0', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + overflow: 'hidden', + textAlign: 'center', + ...theme.font.body16Semibold, }, }); -export { KeywordsContainer, TitleWrapper, Title, KeywordList, KeywordItemConainer, KeywordItem }; +export { KeywordsContainer, KeywordsGrid, KeywordItem }; diff --git a/src/components/Home/Keywords/Keywords.tsx b/src/components/Home/Keywords/Keywords.tsx index c96871e2..9027a1dd 100644 --- a/src/components/Home/Keywords/Keywords.tsx +++ b/src/components/Home/Keywords/Keywords.tsx @@ -1,46 +1,38 @@ -import { useState } from 'react'; import { STOCK_UPDATE_TIME } from '@ts/Constants'; -import { useQueryComponent } from '@hooks/useQueryComponent'; +import { StockCountryKey } from '@ts/StockCountry'; +import useModal from '@hooks/useModal'; import KeywordPopUp from '@components/PopUp/KeywordPopUp/KeywordPopUp'; -import { useKeywordsQuery } from '@controllers/query'; +import { usePopularKeywordsQuery } from '@controllers/stocks/query'; import InfoSVG from '@assets/info.svg?react'; -import { - KeywordItem, - KeywordItemConainer, - KeywordList, - KeywordsContainer, - Title, - TitleWrapper, -} from './Keywords.style'; +import { HomeItemTtile } from '../Title/Title.Style'; +import { KeywordItem, KeywordsContainer, KeywordsGrid } from './Keywords.style'; -const Keywords = ({ country }: { country: string }) => { - const [keywords, suspend] = useQueryComponent({ query: useKeywordsQuery(country) }); +const Keywords = ({ country, onClick }: { country: StockCountryKey; onClick: (keyword: string) => () => void }) => { + const { data: keywords } = usePopularKeywordsQuery(country); - const [isPopupOpen, setPopupOpen] = useState(false); - const togglePopup = () => setPopupOpen((prev) => !prev); + const { Modal, openModal } = useModal({ + Component: KeywordPopUp, + }); const updateTime = STOCK_UPDATE_TIME[country]; + return ( - - - 오늘 가장 많이 언급된 키워드 - <InfoSVG onClick={togglePopup} /> - - 매일 {updateTime}시 업데이트됩니다. - - - - {suspend || - (keywords && - keywords.map((keyword: string, index: number) => ( - {}}> - {keyword.toLocaleUpperCase()} - - )))} - - - {isPopupOpen && } + +

가장 많이 언급되는 키워드

+ +

어제 {updateTime} 기준

+ +
+ { + + {keywords?.slice(0, 9).map((keyword: string) => ( + +

{keyword}

+
+ ))} +
+ }
); }; diff --git a/src/components/Home/StockTable/StockTable.style.ts b/src/components/Home/StockTable/StockTable.style.ts index 6d6aef48..03cd2cda 100644 --- a/src/components/Home/StockTable/StockTable.style.ts +++ b/src/components/Home/StockTable/StockTable.style.ts @@ -1,195 +1,161 @@ import styled from '@emotion/styled'; import { deltaScoreToColor } from '@utils/ScoreConvert'; -import { media, theme } from '@styles/themes'; +import { theme } from '@styles/themes'; const StockTableContainer = styled.div({ - boxSizing: 'border-box', - width: '100%', - - [media[0]]: { - padding: '0 20px', - }, + display: 'flex', + flexDirection: 'column', + gap: '16px', }); -const StockTableTitle = styled.div({ +const StockTableContent = styled.div({ + padding: '0 20px', display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - margin: 0, - padding: '10px 0', + flexDirection: 'column', + gap: '24px', +}); - color: theme.colors.grayscale10, - fontWeight: '700', - fontSize: '32px', - fontFamily: 'Pretendard', - lineHeight: '1.5', +const StockTableTabContainer = styled.div({ + display: 'flex', + gap: '8px', + padding: '0px 8px', +}); - ['div']: { - display: 'flex', - alignItems: 'center', - }, +const StockTableTabLabel = styled.label({ + display: 'flex', + gap: '8px', + width: '100%', - ['span']: { - color: theme.colors.grayscale60, - fontWeight: '500', - fontSize: '15px', + ['>input']: { + display: 'none', }, - ['svg']: { - width: 'auto', - height: '28px', - marginLeft: '8px', + ['>span']: { + ...theme.font.body16Medium, + color: theme.colors.sub_gray6, + background: theme.colors.sub_gray11, + textAlign: 'center', + width: '100%', + padding: '8px 0px', + borderRadius: '8px', }, - [media[0]]: { - padding: '5px 0', - - fontSize: '24px', - - ['span']: { - fontSize: '11px', - }, - - ['svg']: { - height: '0.9em', + ['> input[type="radio"]:checked']: { + ['~span']: { + color: theme.colors.sub_white, + background: theme.colors.sub_blue6, }, }, }); -const StyledTabMenu = styled.ul({ - display: 'flex', - alignItems: 'center', - margin: '0', - padding: 0, - - backgroundColor: theme.colors.primary100, - - '.focused': { - fontWeight: '700', - - backgroundColor: theme.colors.grayscale100, - }, - - '.submenu': { - display: 'flex', - justifyContent: 'center', - boxSizing: 'border-box', - padding: '8px 12px', - - color: theme.colors.primary0, - fontWeight: '500', +const StockTableTable = styled.table({ + width: '100%', + borderCollapse: 'collapse', - backgroundColor: theme.colors.primary100, - borderRadius: '8px', - cursor: 'pointer', + ['>thead>tr, >tbody>tr']: { + display: 'grid', + gridTemplateColumns: 'repeat(3, 1fr)', }, - [media[0]]: { - alignItems: 'center', - justifyContent: 'space-around', + ['>thead>tr>th']: { + ...theme.font.body14Medium, + color: theme.colors.sub_gray6, }, }); -const HeaderItem = styled.div({ - flex: 1, +const StockTableItem = styled.tr({ + padding: '9px 0', - textAlign: 'center', + ['&:not(:last-of-type)']: { + borderBottom: `1px solid ${theme.colors.grayscale90}`, + }, }); -const TableHeaderContainer = styled.div({ +const StockTableItemSymbol = styled.td({ display: 'flex', alignItems: 'center', - justifyContent: 'space-between', - padding: '10px 0', - - color: theme.colors.grayscale60, - fontWeight: 500, - fontSize: '16px', - fontFamily: 'Pretendard', - fontStyle: 'normal', - lineHeight: '1.5', - - [media[0]]: { - padding: '5px 0', - - fontSize: '12px', + gap: '8px', + minWidth: '0', + + ['>img']: { + width: '26px', + height: '26px', + flexShrink: '0', + aspectRatio: '1 / 1', + borderRadius: '50%', }, -}); - -const TableRow = styled.div({ - display: 'grid', - gridTemplateColumns: '33% 33% 33%', - alignItems: 'center', - justifyContent: 'center', - padding: '12px 0', - color: theme.colors.primary0, - - borderBottom: `1px solid ${theme.colors.grayscale90}`, - cursor: 'pointer', - - ':last-child': { - borderBottom: 'none', + ['>p']: { + ...theme.font.body14Medium, + color: theme.colors.primary0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + margin: '0', }, }); -const StockInfo = styled.div({ - display: 'flex', - gap: '8px', - alignItems: 'center', - justifyContent: 'center', - width: '100%', - - textAlign: 'center', -}); +const StockTableItemPrice = styled.td( + ({ delta }: { delta: number }) => ({ + ['>p.diff']: { + color: deltaScoreToColor(delta) ?? theme.colors.sub_gray7, + }, + }), + { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', -const StockLogo = styled.div({ - width: '1.5em', - height: '1.5em', + ['>p']: { + margin: '0', - borderRadius: '64px', -}); + ['&.price']: { + ...theme.font.body14Medium, + color: theme.colors.primary0, + }, -const StockName = styled.div({ - overflow: 'hidden', + ['&.diff']: { + ...theme.font.detail12Medium, + }, + }, + }, +); - textOverflow: 'ellipsis', - wordBreak: 'keep-all', -}); +const StockTableItemScore = styled.td( + ({ delta }: { delta: number }) => ({ + ['>p.diff']: { + color: deltaScoreToColor(delta) ?? theme.colors.sub_gray7, + }, + }), + { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: '2px', -const StockData = styled.div({ - display: 'flex', - flex: 1, - flexDirection: 'column', - gap: '0', - justifyContent: 'center', - - color: theme.colors.primary0, - fontWeight: 500, - fontSize: '17px', - fontFamily: 'Pretendard', - fontStyle: 'normal', - lineHeight: '1.5', - textAlign: 'center', -}); + ['>p']: { + margin: '0', -const DeltaScore = styled.span(({ delta }: { delta: number }) => ({ - gap: '8px', + ['&.score']: { + ...theme.font.body16Semibold, + color: theme.colors.primary0, + }, - color: deltaScoreToColor(delta), - fontSize: '13px', -})); + ['&.diff']: { + ...theme.font.detail12Medium, + }, + }, + }, +); export { StockTableContainer, - StockInfo, - StockName, - StockTableTitle, - StyledTabMenu, - TableHeaderContainer, - HeaderItem, - TableRow, - StockLogo, - StockData, - DeltaScore, + StockTableContent, + StockTableTabContainer, + StockTableTabLabel, + StockTableTable, + StockTableItem, + StockTableItemSymbol, + StockTableItemPrice, + StockTableItemScore, }; diff --git a/src/components/Home/StockTable/StockTable.tsx b/src/components/Home/StockTable/StockTable.tsx index bef6160a..0312a4c8 100644 --- a/src/components/Home/StockTable/StockTable.tsx +++ b/src/components/Home/StockTable/StockTable.tsx @@ -1,101 +1,101 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { STOCK_UPDATE_TIME } from '@ts/Constants'; +import { STOCK_COUNTRY_MAP, StockCountryKey } from '@ts/StockCountry'; +import { diffToPercent, diffToValue } from '@utils/ScoreConvert'; import { useQueryComponent } from '@hooks/useQueryComponent'; import { webPath } from '@router/index'; -import { StockTableInfo } from '@controllers/api.Type'; -import { useStockTableInfoQuery } from '@controllers/query'; -import HumanIndexSVG from '@assets/HumanIndex.svg?react'; +import StockImage from '@components/Common/StockImage'; +import { StockTableInfo } from '@controllers/stocks/types'; +import { useStockTableInfoQuery } from '@controllers/stocks/query'; +import { HomeItemTtile } from '../Title/Title.Style'; import { - DeltaScore, - HeaderItem, - StockData, - StockInfo, // StockLogo, - StockName, StockTableContainer, - StockTableTitle, - StyledTabMenu, - TableHeaderContainer, - TableRow, + StockTableContent, + StockTableItem, + StockTableItemPrice, + StockTableItemScore, + StockTableItemSymbol, + StockTableTabContainer, + StockTableTabLabel, + StockTableTable, } from './StockTable.style'; -const tabMenu = ['시가총액', '거래량', '급상승', '급하락']; -const categories = ['MARKET', 'VOLUME', 'RISING', 'DESCENT']; +const StockTableTab = [ + { key: 'MARKET', text: '거래대금' }, + { key: 'VOLUME', text: '거래량' }, + { key: 'RISING', text: '급상승' }, + { key: 'DESCENT', text: '급하락' }, +]; -const StockTable = ({ country }: { country: string }) => { +const StockTable = ({ country }: { country: StockCountryKey }) => { const navigate = useNavigate(); - const [tabIndex, setTabIndex] = useState(0); - + const [tableTab, setTableTab] = useState('MARKET'); const updateTime = STOCK_UPDATE_TIME[country]; + const currency = STOCK_COUNTRY_MAP[country].currency; const [stockTable, suspend] = useQueryComponent({ - query: useStockTableInfoQuery(categories[tabIndex], country), + query: useStockTableInfoQuery(tableTab, country), }); - const handleClick = (name: string) => { + const handleClick = (name: string) => () => { navigate(webPath.search(), { state: { symbolName: name, country: country } }); }; - const handleTab = (index: number) => { - if (tabIndex === index) { - return; - } - - setTabIndex(index); + const handleTabChange = (e: React.ChangeEvent) => { + setTableTab(e.target.value); }; return ( - -
- 종목 차트별 -
- 매일 {updateTime}시 업데이트됩니다. -
- - {tabMenu.map((el, index) => ( -
  • handleTab(index)} - > - {el} -
  • - ))} -
    - - - 종목 - 주가 - 인간지표 - - {suspend || - (stockTable && - stockTable.map((stock: StockTableInfo, index: number) => { - return ( - handleClick(stock.symbolName)}> - - - {/* */} - {stock.symbolName} - - - - {stock.price.toLocaleString()} - - {stock.priceDiff > 0 ? '+' : ''} - {stock.priceDiff.toLocaleString()}({Math.abs(stock.priceDiffPerCent)}%) - - - - {stock.score}점 - - ({stock.scoreDiff > 0 ? '+' : ''} - {stock.scoreDiff}) - - - - ); - }))} + +

    종목 차트별 인간지표

    +

    어제 {updateTime} 기준

    +
    + + + {StockTableTab.map(({ key, text }, idx) => ( + + + {text} + + ))} + + {suspend || ( + + + + 종목 + 주가 + 인간지표 + + + + {stockTable?.map((stock: StockTableInfo) => ( + + + +

    {stock.symbolName}

    +
    + +

    {currency}{stock.price.toLocaleString()}

    +

    + {diffToValue(stock.priceDiff)}( + {diffToPercent(stock.price, stock.priceDiff, { fixed: 2, sign: false })}) +

    +
    + +

    {stock.score}점

    +

    ({diffToValue(stock.scoreDiff)}점)

    +
    +
    + ))} + +
    + )} +
    ); }; diff --git a/src/components/Home/Title/Title.Style.ts b/src/components/Home/Title/Title.Style.ts new file mode 100644 index 00000000..8b415e42 --- /dev/null +++ b/src/components/Home/Title/Title.Style.ts @@ -0,0 +1,38 @@ +import styled from '@emotion/styled'; +import { theme } from '@styles/themes'; + +const HomeItemTtile = styled.div({ + display: 'flex', + padding: '0px 20px', + gap: '6px', + alignItems: 'center', + + ['>p']: { + margin: '0px', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + + ['&.title']: { + ...theme.font.title20Semibold, + color: theme.colors.sub_gray2, + flexShrink: '0', + }, + + ['&.update-time']: { + ...theme.font.body14Regular, + color: theme.colors.sub_gray8, + marginLeft: 'auto', + }, + }, + + ['>svg']: { + width: '18px', + height: 'auto', + aspectRatio: '1 / 1', + fill: theme.colors.sub_gray6, + flexShrink: '0', + }, +}); + +export { HomeItemTtile }; diff --git a/src/components/Lab/Common.Style.ts b/src/components/Lab/Common.Style.ts new file mode 100644 index 00000000..1e9774cd --- /dev/null +++ b/src/components/Lab/Common.Style.ts @@ -0,0 +1,163 @@ + +import styled from '@emotion/styled'; +import { theme } from '@styles/themes'; + +export const Container = styled.div` + background: black; + color: ${theme.colors.sub_white}; + display: flex; + flex-direction: column; + height: 100vh; +`; + +export const TopBar = styled.div<{ statusRate: number }>` + position: relative; + display: flex; + align-items: center; + justify-content: center; + height: 64px; + padding: 24px; + box-sizing: border-box; + border-bottom: 4px solid ${theme.colors.sub_gray11}; + + &::before { + content: ''; + position: absolute; + bottom: -4px; + left: 0; + height: 4px; + width: ${({ statusRate }) => `${statusRate}%`}; + background: ${theme.colors.sub_gray9}; + } +`; + + +export const BackIcon = styled.div` + position: absolute; + left: 20px; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + padding-left: 8px; +`; + +export const TopBarTitle = styled.div` + ${theme.font.body18Semibold}; + color: ${theme.colors.sub_gray5}; +`; + +export const TabContainer = styled.div` + border-bottom: 1px solid ${theme.colors.sub_gray6}; + padding: 0 24px; +`; + +export const InnerContainer = styled.div` + padding: 24px; + padding-bottom: 120px; + overflow-y: auto; + justify-content: space-between; +`; + +export const Title = styled.div` + ${theme.font.title20Semibold} + margin-bottom: 12px; +`; + +export const Description = styled.p` + ${theme.font.body14Medium}; + color: ${theme.colors.sub_gray6}; + margin-bottom: 36px; +`; + +export const NavButtonContainer = styled.div` + display: flex; + justify-content: space-between; + margin-top: 100px; +`; + +export const NavButton = styled.button<{ next?: boolean, active?: boolean }>` + ${theme.font.body18Semibold}; + flex: 1; + padding: 12px; + margin: 0 4px; + border-radius: 8px; + border: none; + background: ${({ next, active }) => active ? theme.colors.sub_blue6 : next ? theme.colors.sub_gray8 : theme.colors.sub_gray11}; + color: ${({ next, active }) => active ? theme.colors.sub_white : next ? theme.colors.sub_black : theme.colors.sub_gray5}; + +`; + +export const IndustryTag = styled.div<{ selected: boolean }>` + padding: 8px 16px; + background: ${({ selected }) => selected ? theme.colors.sub_blue6 : theme.colors.sub_gray10}; + color: ${({ selected }) => selected ? theme.colors.sub_white : theme.colors.sub_gray6}; + border-radius: 50px; +`; + + +export const SearchInput = styled.input` + background: transparent; + border: none; + outline: none; + flex: 1; + color: white; + + &::placeholder{ + ${theme.font.body16Medium} + color: ${theme.colors.sub_gray7}; + } +`; + +export const SearchIconWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + color: ${theme.colors.sub_gray8}; +`; + +export const SearchBar = styled.div` + background: ${theme.colors.sub_gray11}; + color:white; + border-radius: 8px; + display: flex; + align-items: center; + padding: 10px 12px; +`; + + +export const ToastStyle = styled.div` + ${theme.font.detail12Semibold} + color:${theme.colors.sub_gray2}; + position: fixed; + bottom: 100px; + left: 50%; + transform: translateX(-50%); + padding: 12px 16px; + background: rgba(0, 0, 0, 0.80); + border: 1px solid rgba(73, 80, 87, 0.5); + border-radius: 5px; + z-index: 1000; + + max-width: 80vw; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + display: flex; + align-items: center; + justify-content: center; +`; + + +export const Divider = styled.div` + background-color: ${theme.colors.sub_gray11}; + width: calc(100% + 48px); + border: none; + height: 4px; + margin: 24px -24px; +`; + +export const StatusTitle = styled.p` + ${theme.font.title20Medium} +`; diff --git a/src/components/Lab/ExperimentItem/ExperimentItem.Style.ts b/src/components/Lab/ExperimentItem/ExperimentItem.Style.ts new file mode 100644 index 00000000..99e58e59 --- /dev/null +++ b/src/components/Lab/ExperimentItem/ExperimentItem.Style.ts @@ -0,0 +1,76 @@ +import styled from '@emotion/styled'; +import { deltaScoreToColor } from '@utils/ScoreConvert'; +import { theme } from '@styles/themes'; + +const ExperimentItemContainer = styled.div({ + display: 'flex', + alignItems: 'center', + + ['>p.index']: { + width: '32px', + margin: '0px', + ...theme.font.body14Semibold, + color: theme.colors.sub_blue6, + flexShrink: '0', + textAlign: 'center', + }, + + ['>button.more']: { + ...theme.font.body14Semibold, + background: theme.colors.sub_blue6, + color: theme.colors.sub_white, + borderRadius: '5px', + padding: '4px 16px', + border: 'none', + flexShrink: '0', + }, +}); + +const ExperimentItemContent = styled.div({ + display: 'flex', + padding: '6px 10px', + alignItems: 'center', + gap: '12px', + overflow: 'hidden', + flexGrow: 1, + + ['>img']: { + width: '28px', + height: '28px', + aspectRatio: '1 / 1', + borderRadius: '999px', + objectFit: 'contain', + }, + + ['>div']: { + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + + ['>p']: { + margin: '0', + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + + ['&.name']: { + ...theme.font.body14Semibold, + color: theme.colors.sub_gray1, + }, + ['&.date']: { + ...theme.font.detail12Medium, + color: theme.colors.sub_gray6, + }, + ['&.diff']: { + ...theme.font.detail12Medium, + color: theme.colors.sub_gray6, + }, + }, + }, +}); + +const ColoredDiffLabel = styled.span(({ delta }: { delta: number }) => ({ + color: deltaScoreToColor(delta) ?? theme.colors.sub_gray7, +})); + +export { ExperimentItemContainer, ExperimentItemContent, ColoredDiffLabel }; diff --git a/src/components/Lab/ExperimentItem/ExperimentItem.tsx b/src/components/Lab/ExperimentItem/ExperimentItem.tsx new file mode 100644 index 00000000..348382ea --- /dev/null +++ b/src/components/Lab/ExperimentItem/ExperimentItem.tsx @@ -0,0 +1,54 @@ +import StockImage from '@components/Common/StockImage'; +import { ExperimentItem } from '@controllers/experiment/api'; +import { ColoredDiffLabel, ExperimentItemContainer, ExperimentItemContent } from './ExperimentItem.Style'; + +const ExperimentItemComponent = ({ + experiment, + idx, + handleClickExperimentDetail, +}: { + experiment: ExperimentItem; + idx: number; + handleClickExperimentDetail: (experimentId: number) => void; +}) => { + const { stockId, symbolName, buyAt, status, buyPrice, roi } = experiment; + + const dateText = ((date: Date) => { + const [year, month, day] = [date.getFullYear(), date.getMonth() + 1, date.getDate()].map((num) => + num.toString().padStart(2, '0'), + ); + + return `${year}.${month}.${day}`; + })(new Date(buyAt)); + const statusText = status === 'PROGRESS' ? '실험중' : '완료'; + + const daysAgo = Math.floor((new Date().getTime() - new Date(buyAt).getTime()) / (24 * 60 * 60 * 1000)); + + const diff = roi; + const sign = diff > 0 ? '+' : diff < 0 ? '-' : ''; + const diffPercent = (Math.abs(diff / buyPrice) * 100).toFixed(1); + const diffPercentText = sign + diffPercent + '%'; + + return ( + +

    {idx + 1}

    + + +
    +

    {symbolName}

    +

    + {dateText}({statusText}) +

    +

    + {daysAgo}일전보다 {diffPercentText} +

    +
    +
    + +
    + ); +}; + +export default ExperimentItemComponent; diff --git a/src/components/Lab/ReportClassChart/ReportClassChart.Style.ts b/src/components/Lab/ReportClassChart/ReportClassChart.Style.ts new file mode 100644 index 00000000..5e1f4d71 --- /dev/null +++ b/src/components/Lab/ReportClassChart/ReportClassChart.Style.ts @@ -0,0 +1,93 @@ +import styled from '@emotion/styled'; +import { theme } from '@styles/themes'; + +const Container = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '4px', +}); + +const Content = styled.div({ + width: '100%', + height: '200px', + position: 'relative', + + ['>canvas']: { + width: '100%', + height: '100%', + }, +}); + +const TooltipLine = styled.span( + ({ successRate }: { successRate: number }) => ({ + left: `${((1 + (successRate / 100) * 20) / 22) * 100}%`, + }), + { + position: 'absolute', + top: '0', + bottom: '0', + transform: 'translateX(-50%)', + width: '2px', + height: '100%', + background: 'rgba(255, 255, 255, 0.12)', + backgroundImage: `linear-gradient(to bottom, ${theme.colors.sub_white}80 0px, ${theme.colors.sub_blue7} 100%)`, + maskImage: `repeating-linear-gradient(to bottom, #FFFFFF, #FFFFFF 3px, transparent 3px, transparent 6px)`, + webkitMaskImage: `repeating-linear-gradient(to bottom, #FFFFFF, #FFFFFF 3px, transparent 3px, transparent 6px)`, + }, +); + +const TooltipContainer = styled.div( + ({ successRate, width }: { successRate: number; width: number }) => { + return { + left: `clamp(calc(0% + ${width / 2}px), ${successRate}%, calc(100% - ${width / 2}px))`, + }; + }, + { + position: 'absolute', + top: '0', + color: theme.colors.sub_white, + transform: `translateX(-50%)`, + boxSizing: 'border-box', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: '10px 16px', + background: 'rgba(255, 255, 255, 0.12)', + border: '1px solid rgba(255, 255, 255, 0.1)', + backdropFilter: 'blur(10px)', + borderRadius: '999px', + whiteSpace: 'nowrap', + + ['>p']: { + margin: '0', + + ['&.title']: { + ...theme.font.detail12Semibold, + color: theme.colors.sub_gray4, + + ['>b']: { + ...theme.font.detail12Semibold, + color: theme.colors.sub_white, + }, + }, + + ['&.description']: { + ...theme.font.detail10Medium, + color: theme.colors.sub_gray4, + }, + }, + }, +); + +const IndexContainer = styled.div({ + display: 'flex', + + ['>span']: { + ...theme.font.body14Medium, + color: theme.colors.sub_gray6, + width: '100%', + textAlign: 'center', + }, +}); + +export { Container, Content, TooltipLine, TooltipContainer, IndexContainer }; diff --git a/src/components/Lab/ReportClassChart/ReportClassChart.Type.tsx b/src/components/Lab/ReportClassChart/ReportClassChart.Type.tsx new file mode 100644 index 00000000..266ea34b --- /dev/null +++ b/src/components/Lab/ReportClassChart/ReportClassChart.Type.tsx @@ -0,0 +1,100 @@ +import { theme } from '@styles/themes'; + +export type ReportClassKey = 'worst' | 'bad' | 'normal' | 'good' | 'best'; + +export interface ReportClassType { + emoji: string; + title: string; + description: React.ReactNode; + color: keyof typeof theme.colors; + min: number; + max: number; + range: string; +} + +export const reportClassMap: Record = { + worst: { + emoji: '😱', + title: '완전 인간 아님', + description: ( + <> + 성공률이 0~20%인 유형을 말해요 +
    + 유저 중 N%가 이에 속한답니다 + + ), + color: 'sub_red', + min: 0, + max: 20, + range: '0~20%', + }, + bad: { + emoji: '🙁', + title: '인간 아님', + description: ( + <> + 성공률이 20~40%인 유형을 말해요
    + 유저 중 N%가 이에 속한답니다 + + ), + color: 'sub_red', + min: 20, + max: 40, + range: '20~40%', + }, + normal: { + emoji: '😐', + title: '평범 인간', + description: ( + <> + 성공률이 40~60%인 유형을 말해요 +
    + 유저 중 N%가 이에 속한답니다 + + ), + color: 'sub_gray9', + min: 40, + max: 60, + range: '40~60%', + }, + good: { + emoji: '☺️', + title: '인간 맞음', + description: ( + <> + 성공률이 60~80%인 유형을 말해요 +
    + 유저 중 N%가 이에 속한답니다 + + ), + color: 'sub_gray1', + min: 60, + max: 80, + range: '60~80%', + }, + best: { + emoji: '😆', + title: '인간 완전 맞음', + description: ( + <> + 성공률이 80%이상인 유형을 말해요 +
    + 유저 중 N%가 이에 속한답니다 + + ), + color: 'sub_white', + min: 80, + max: Infinity, + range: '80% 이상', + }, +}; + +export const reportClassList: ({ + key: ReportClassKey; +} & ReportClassType)[] = Object.entries(reportClassMap).map( + ([key, value]) => + ({ + key, + ...value, + }) as { key: ReportClassKey } & ReportClassType, +); diff --git a/src/components/Lab/ReportClassChart/ReportClassChart.tsx b/src/components/Lab/ReportClassChart/ReportClassChart.tsx new file mode 100644 index 00000000..de986daf --- /dev/null +++ b/src/components/Lab/ReportClassChart/ReportClassChart.tsx @@ -0,0 +1,96 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { theme } from '@styles/themes'; +import { Container, Content, IndexContainer, TooltipContainer, TooltipLine } from './ReportClassChart.Style'; +import { ReportClassType } from './ReportClassChart.Type'; + +const ReportClassChart = ({ + reportClass, + successRate, + sameGradeUserRate, +}: { + reportClass: ReportClassType; + successRate: number; + sameGradeUserRate: number; +}) => { + const canvasRef = useRef(null); + const tooltipRef = useRef(null); + + const [tooltipWidth, setTooltipWidth] = useState(0); + + useEffect(() => { + if (tooltipRef.current) { + setTooltipWidth(tooltipRef.current.clientWidth); + } + }, [tooltipRef]); + + const fx = (x: number, mu: number, sigma: number) => { + return Math.exp(-Math.pow((x - mu) / sigma, 2) / 2) / (sigma * Math.sqrt(2 * Math.PI)); + }; + + const drawChart = useCallback( + (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => { + const { width, height } = canvas; + + ctx.clearRect(0, 0, width, height); + ctx.lineWidth = 2; + ctx.strokeStyle = theme.colors.sub_blue7; + + ctx.beginPath(); + for (let x = 0; x <= 100; x += 1) { + const y = fx(x, 50, 20); + const pos: [number, number] = [(x * width) / 100, (1 - y / 0.025) * height]; + ctx[x === 0 ? 'moveTo' : 'lineTo'](...pos); + } + ctx.stroke(); + + ctx.lineTo(width, height); + ctx.lineTo(0, height); + + ctx.closePath(); + + const gradient = ctx.createLinearGradient(0, 0, 0, height); + gradient.addColorStop(0, theme.colors.sub_blue5); + gradient.addColorStop(1, `${theme.colors.sub_blue5}00`); + ctx.fillStyle = gradient; + + ctx.fill(); + }, + [canvasRef], + ); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) { + return; + } + + const ctx = canvas.getContext('2d'); + if (!ctx) { + return; + } + + drawChart(ctx, canvas); + }, []); + + return ( + + + + + +

    + 성공률 {reportClass.range} +

    +

    (전체 유저 중 {sameGradeUserRate.toFixed(1)}%)

    +
    +
    + + {Array.from({ length: 11 }).map((_, idx) => ( + {idx} + ))} + +
    + ); +}; + +export default ReportClassChart; diff --git a/src/components/Lab/ReportPatternChart/ReportPatternChart.Style.ts b/src/components/Lab/ReportPatternChart/ReportPatternChart.Style.ts new file mode 100644 index 00000000..9c907497 --- /dev/null +++ b/src/components/Lab/ReportPatternChart/ReportPatternChart.Style.ts @@ -0,0 +1,181 @@ +import styled from '@emotion/styled'; +import { theme } from '@styles/themes'; +import { PatternQuadrantKey } from './ReportPatternChart.Type'; + +const ReportPatternChartContainer = styled.div({ + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + + ['>div']: { + position: 'relative', + height: '200px', + width: '100%', + + ['>canvas']: { + width: '100%', + height: '100%', + }, + }, +}); + +const ReportPatternChartAxisLabel = styled.p( + ({ isTutorial }: { isTutorial?: boolean }) => ({ + ['&.roi']: { + color: isTutorial ? theme.colors.sub_blue6 : theme.colors.sub_gray6, + }, + + ['&.index']: { + color: isTutorial ? theme.colors.sub_red : theme.colors.sub_gray6, + }, + }), + { + margin: '0px', + ...theme.font.body16Semibold, + whiteSpace: 'nowrap', + + ['&.index']: { + position: 'absolute', + right: '0', + top: 'calc(50% + 8px)', + }, + }, +); + +const ReportPatternChartItem = styled.div( + ({ x, y, quadrant }: { x: number; y: number; quadrant: PatternQuadrantKey }) => { + const inQuadrant = + x - 50 >= 0 + ? y - 50 >= 0 + ? quadrant === 'trend-preemptive' + : quadrant === 'lagging-follower' + : y - 50 >= 0 + ? quadrant === 'value-preemptive' + : quadrant === 'reverse-investor'; + + return { + left: `${x}%`, + bottom: `${y}%`, + + ['>p']: { + color: inQuadrant ? theme.colors.sub_gray2 : theme.colors.sub_gray7, + }, + }; + }, + { + position: 'absolute', + width: '0px', + height: '0px', + + ['>p']: { + margin: '0', + position: 'absolute', + bottom: 'calc(50% + 16px)', + left: '50%', + transform: 'translate(-50%, 50%)', + ...theme.font.detail12Medium, + }, + + ['>span']: { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: '10px', + height: '10px', + borderRadius: '50%', + background: theme.colors.sub_blue6, + }, + }, +); + +const ReportPatternChartQuadrant = styled.div( + ({ quadrant }: { quadrant: PatternQuadrantKey }) => ({ + top: ['trend-preemptive', 'value-preemptive'].includes(quadrant) ? '0' : '', + left: ['value-preemptive', 'reverse-investor'].includes(quadrant) ? '0' : '', + right: ['trend-preemptive', 'lagging-follower'].includes(quadrant) ? '0' : '', + bottom: ['reverse-investor', 'lagging-follower'].includes(quadrant) ? '0' : '', + }), + { + position: 'absolute', + width: 'calc(50% - 6px)', + height: 'calc(50% - 6px)', + opacity: 0.2, + padding: '6px', + boxSizing: 'border-box', + background: theme.colors.sub_blue6, + borderRadius: '4px', + }, +); + +const ReportPatternChartTutorialQuadrant = styled.div({ + position: 'absolute', + width: '100%', + height: '100%', + top: '0', + left: '0', + + ['>div']: { + position: 'absolute', + width: '50%', + height: '50%', + display: 'flex', + flexDirection: 'column', + gap: '12px', + alignItems: 'start', + justifyContent: 'center', + padding: '12px', + boxSizing: 'border-box', + + ['&.trend-preemptive']: { + top: '0', + right: '0', + }, + ['&.lagging-follower']: { + right: '0', + bottom: '0', + }, + ['&.reverse-investor']: { + left: '0', + bottom: '0', + }, + ['&.value-preemptive']: { + top: '0', + left: '0', + }, + + ['>div']: { + display: 'flex', + flexDirection: 'column', + + ['>p']: { + margin: '0', + ['&.title']: { + ...theme.font.detail12Semibold, + color: theme.colors.sub_white, + }, + ['&.description']: { + ...theme.font.detail12Medium, + color: theme.colors.sub_gray6, + }, + }, + }, + + ['>span']: { + ...theme.font.detail10Semibold, + color: theme.colors.sub_white, + padding: '4px 8px', + background: theme.colors.sub_gray9, + borderRadius: '999px', + }, + }, +}); + +export { + ReportPatternChartContainer, + ReportPatternChartAxisLabel, + ReportPatternChartItem, + ReportPatternChartQuadrant, + ReportPatternChartTutorialQuadrant, +}; diff --git a/src/components/Lab/ReportPatternChart/ReportPatternChart.Type.tsx b/src/components/Lab/ReportPatternChart/ReportPatternChart.Type.tsx new file mode 100644 index 00000000..73abb73f --- /dev/null +++ b/src/components/Lab/ReportPatternChart/ReportPatternChart.Type.tsx @@ -0,0 +1,77 @@ +export type PatternQuadrantKey = 'trend-preemptive' | 'lagging-follower' | 'reverse-investor' | 'value-preemptive'; + +export const patternQuadrantKeys: PatternQuadrantKey[] = [ + 'trend-preemptive', + 'lagging-follower', + 'reverse-investor', + 'value-preemptive', +]; + +export interface PatternQuadrant { + emoji: string; + title: string; + score: string; + roi: string; + description: React.ReactElement; +} + +export const patternQuadrantMap: Record = { + 'trend-preemptive': { + emoji: '✅', + title: '트렌드 선점형', + score: '인간지표 높을 때 매수', + roi: '수익', + description: ( + <> + 투자자들의 관심도가 높을 때 매수하여 수익을 보는 투자 패턴
    = 트렌드 형성 시점에 선제적으로 대응하는 투자 + 성향을 보이고 있네요! + + ), + }, + 'lagging-follower': { + emoji: '❕', + title: '후행 추종형', + score: '인간지표 높을 때 매수', + roi: '손실', + description: ( + <> + 투자자들의 관심도가 높을 때 매수하여 손실을 보는 투자 패턴
    = 과열 국면에서 진입해 변동성 영향을 크게 받는 + 투자 성향을 보이고 있네요! + + ), + }, + 'reverse-investor': { + emoji: '📉', + title: '역행 투자형', + score: '인간지표 낮을 때 매수', + roi: '손실', + description: ( + <> + 투자자들의 관심도가 낮을 때 매수하여 손실을 보는 투자 패턴
    + =진입 시점하는 타이밍이 시장 흐름과 맞지 않는 경우가 나타나고 있네요! + + ), + }, + 'value-preemptive': { + emoji: '💎', + title: '가치 선점형', + score: '인간지표 낮을 때 매수', + roi: '수익', + description: ( + <> + 투자자들의 관심도가 낮을 때 매수하여 수익을 보는 투자 패턴
    + =저평가 구간에서 기회를 선점하는 투자 성향을 보이고 있네요! + + ), + }, +}; + +export const patternQuadrantList: ({ + key: PatternQuadrantKey; +} & PatternQuadrant)[] = Object.entries(patternQuadrantMap).map( + ([key, value]) => + ({ + key, + ...value, + }) as { key: PatternQuadrantKey } & PatternQuadrant, +); diff --git a/src/components/Lab/ReportPatternChart/ReportPatternChart.tsx b/src/components/Lab/ReportPatternChart/ReportPatternChart.tsx new file mode 100644 index 00000000..5b69d09f --- /dev/null +++ b/src/components/Lab/ReportPatternChart/ReportPatternChart.tsx @@ -0,0 +1,127 @@ +import { useEffect, useRef, useState } from 'react'; +import { PortfolioResultPatternHistory } from '@controllers/experiment/api'; +import { theme } from '@styles/themes'; +import { + ReportPatternChartAxisLabel, + ReportPatternChartContainer, + ReportPatternChartItem, + ReportPatternChartQuadrant, + ReportPatternChartTutorialQuadrant, +} from './ReportPatternChart.Style'; +import { PatternQuadrantKey, patternQuadrantList } from './ReportPatternChart.Type'; + +const ReportPatternChart = ({ + reportPatternsQuadrant = 'value-preemptive', + reportPatternHistory = [], + isTutorial = false, +}: { + reportPatternsQuadrant?: PatternQuadrantKey; + reportPatternHistory?: PortfolioResultPatternHistory[]; + isTutorial?: boolean; +}) => { + const canvasRef = useRef(null); + const [canvasSize, setCanvasSize] = useState<{ width: number; height: number }>({ width: 0, height: 0 }); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + const { width, height } = canvas; + ctx.clearRect(0, 0, width, height); + + ctx.fillStyle = theme.colors.sub_gray10; + ctx.strokeStyle = theme.colors.sub_gray10; + ctx.lineWidth = 2; + + const triangleSide = 12; + const triangleHeight = (Math.sqrt(3) / 2) * triangleSide; + + ctx.beginPath(); + ctx.moveTo(width / 2, 0); + ctx.lineTo(width / 2, height); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(0, height / 2); + ctx.lineTo(width, height / 2); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(width, height / 2); + ctx.lineTo(width - triangleHeight, height / 2 - triangleSide / 2); + ctx.lineTo(width - triangleHeight, height / 2 + triangleSide / 2); + ctx.closePath(); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(width / 2, 0); + ctx.lineTo(width / 2 - triangleSide / 2, triangleHeight); + ctx.lineTo(width / 2 + triangleSide / 2, triangleHeight); + ctx.closePath(); + ctx.fill(); + }, [canvasRef, canvasSize]); + + useEffect(() => { + const container = canvasRef.current; + if (!container) return; + + const resizeObserver = new ResizeObserver((entries) => { + const entry = entries[0]; + if (!entry) return; + + const { width, height } = entry.contentRect; + setCanvasSize({ width, height }); + }); + + resizeObserver.observe(container); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + return ( + + + 수익률 + +
    + + {!isTutorial && } + + 인간지표 + + {isTutorial ? ( + + {patternQuadrantList.map((e) => ( +
    +
    +

    + {e.emoji} {e.title} +

    +

    {e.score}

    +
    + → {e.roi} +
    + ))} +
    + ) : ( + reportPatternHistory.map((e, i) => ( + +

    {e.date}

    + +
    + )) + )} +
    +
    + ); +}; + +export default ReportPatternChart; diff --git a/src/components/Loading/Loading.tsx b/src/components/Loading/Loading.tsx new file mode 100644 index 00000000..bc4fe750 --- /dev/null +++ b/src/components/Loading/Loading.tsx @@ -0,0 +1,88 @@ +import styled from '@emotion/styled'; +import { theme } from '@styles/themes'; +import LoadingWEBM from '@assets/Loading.webm'; +import BackgroundSVG from '@assets/background.svg?react'; + +const LoadingContainer = styled.div( + ({ top, bottom }: { top?: string; bottom?: string }) => ({ + top: top ?? 0, + bottom: bottom ?? 0, + }), + { + position: 'fixed', + bottom: '96px', + left: 0, + right: 0, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + background: 'linear-gradient(180deg, rgba(16, 16, 16, 0.4) 0%, #101010DD 81.02%)', + backdropFilter: 'blur(2.5px)', // Note: backdrop-filter has minimal browser support + zIndex: 9999, + + ['>svg']: { + position: 'absolute', + width: '110%', + height: 'auto', + right: '0', + top: '0', + }, + }, +); + +const LoadingContent = styled.div({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: '16px', + + ['>video']: { + width: '80px', + height: 'auto', + aspectRatio: '1 / 1', + }, + + ['>div']: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: '8px', + + ['>p']: { + margin: '0', + + ['&.title']: { + ...theme.font.title20Semibold, + color: theme.colors.sub_gray4, + }, + + ['&.desc']: { + ...theme.font.body16Regular, + color: theme.colors.sub_gray6, + }, + }, + }, +}); + +const Loading = ({ isLoading, title, desc }: { isLoading?: boolean; title: string; desc?: string }) => { + if (!isLoading) { + return null; + } + + return ( + + + + + + ); +}; + +export default Loading; diff --git a/src/components/Modal/AboutReportClass/AboutReportClass.tsx b/src/components/Modal/AboutReportClass/AboutReportClass.tsx new file mode 100644 index 00000000..1ead78e8 --- /dev/null +++ b/src/components/Modal/AboutReportClass/AboutReportClass.tsx @@ -0,0 +1,155 @@ +import styled from '@emotion/styled'; +import { useState } from 'react'; +import ReportClassChart from '@components/Lab/ReportClassChart/ReportClassChart'; +import { + ReportClassKey, + reportClassList, + reportClassMap, +} from '@components/Lab/ReportClassChart/ReportClassChart.Type'; +import { theme } from '@styles/themes'; +import QuestionMarkSVG from '@assets/icons/questionMark.svg?react'; + +const Container = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '16px', +}); + +const Header = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '4px', + padding: '14px 10px', + borderBottom: `1px solid ${theme.colors.sub_gray10}`, + margin: '0 20px', + + ['>p']: { + margin: '0', + wordBreak: 'keep-all', + + ['&.title']: { + ...theme.font.body18Semibold, + color: theme.colors.sub_gray1, + + ['>svg']: { + width: '24px', + height: 'auto', + aspectRatio: '1 / 1', + fill: theme.colors.sub_white, + flexShrink: '0', + verticalAlign: 'middle', + }, + }, + + ['&.description']: { + ...theme.font.body14Medium, + color: theme.colors.sub_gray7, + }, + }, +}); + +const TabContainer = styled.div({ + display: 'flex', + gap: '16px', + overflow: 'auto', + padding: '0 20px', + + msOverflowStyle: 'none', + ['&::-webkit-scrollbar']: { + display: 'none', + }, +}); + +const TabItem = styled.span( + ({ isSelected }: { isSelected?: boolean }) => ({ + background: isSelected ? theme.colors.sub_blue6 : theme.colors.sub_gray10, + color: isSelected ? theme.colors.sub_white : theme.colors.sub_gray6, + }), + { + position: 'relative', + ...theme.font.body14Medium, + whiteSpace: 'nowrap', + padding: '8px 16px', + borderRadius: '999px', + }, +); + +const Content = styled.div({ + display: 'flex', + flexDirection: 'column', + padding: '20px 10px 12px', + margin: '0 20px', + background: theme.colors.sub_gray11, + borderRadius: '4px', + gap: '20px', + + ['>span.divider']: { + width: '100%', + height: '1px', + background: theme.colors.sub_gray10, + }, +}); + +const ContentHeader = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '8px', + padding: '0 8px', + + ['>p']: { + margin: '0', + + ['&.title']: { + ...theme.font.body14Semibold, + color: theme.colors.sub_gray2, + }, + ['&.description']: { + ...theme.font.detail12Medium, + color: theme.colors.sub_gray5, + lineHeight: '175%', + whiteSpace: 'pre-line', + }, + }, +}); + +const AboutReportClass = () => { + const [isSelected, setIsSelected] = useState('worst'); + + return ( + +
    +

    + 다른 '인간지표' 유형은 뭐가 있어요? +

    +

    + 실험이 끝났을 때 수익률이 0이상인 실험을,
    + 성공한 실험으로 보고 있어요 +

    +
    + + {reportClassList.map((e) => ( + setIsSelected(e.key)} isSelected={isSelected === e.key}> + {e.emoji} {e.title} + + ))} + + + +

    {reportClassMap[isSelected].title} 지표란?

    +

    + {/* 여기 문구 추가해야 함 */} + {reportClassMap[isSelected].description} +

    +
    + + +
    +
    + ); +}; + +export default AboutReportClass; diff --git a/src/components/Modal/AboutReportClass/useAboutReportClass.ts b/src/components/Modal/AboutReportClass/useAboutReportClass.ts new file mode 100644 index 00000000..dc2e4609 --- /dev/null +++ b/src/components/Modal/AboutReportClass/useAboutReportClass.ts @@ -0,0 +1,20 @@ +import BottomUpCancel from '../Layout/BottomUpCancel/BottomUpCancel'; +import useModal from '../useModal'; +import AboutReportClass from './AboutReportClass'; + +const useAboutReportClass = (): { + openModal: () => void; + closeModal: () => void; + Modal: JSX.Element | null; +} => { + const { Modal, openModal, closeModal } = useModal({ + Layout: BottomUpCancel, + Component: AboutReportClass, + modalKey: 'aboutReportClass', + showDelay: 200, + }); + + return { Modal, openModal, closeModal }; +}; + +export default useAboutReportClass; diff --git a/src/components/Modal/AboutReportPattern/AboutReportPattern.tsx b/src/components/Modal/AboutReportPattern/AboutReportPattern.tsx new file mode 100644 index 00000000..6d02c438 --- /dev/null +++ b/src/components/Modal/AboutReportPattern/AboutReportPattern.tsx @@ -0,0 +1,159 @@ +import styled from '@emotion/styled'; +import ReportPatternChart from '@components/Lab/ReportPatternChart/ReportPatternChart'; +import { theme } from '@styles/themes'; +import QuestionMarkSVG from '@assets/icons/questionMark.svg?react'; + +const Container = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '16px', +}); + +const Header = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '16px', + padding: '14px 10px', + margin: '0 20px', + + ['>p']: { + margin: '0', + wordBreak: 'keep-all', + + ['&.title']: { + ...theme.font.body18Semibold, + color: theme.colors.sub_gray1, + + ['>svg']: { + width: '24px', + height: 'auto', + aspectRatio: '1 / 1', + fill: theme.colors.sub_white, + flexShrink: '0', + verticalAlign: 'middle', + }, + }, + }, + + ['>span.divider']: { + width: '100%', + height: '1px', + background: theme.colors.sub_gray10, + }, +}); + +const HeaderContents = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '8px', +}); + +const HeaderContentsItem = styled.div({ + display: 'flex', + alignItems: 'center', + gap: '16px', + + ['>span']: { + width: '72px', + padding: '4px', + borderRadius: '999px', + background: theme.colors.sub_gray11, + ...theme.font.body14Semibold, + textAlign: 'center', + + ['&.roi']: { + color: theme.colors.sub_blue6, + }, + ['&.score']: { + color: theme.colors.sub_red, + }, + }, + + ['>div']: { + display: 'flex', + flexDirection: 'column', + + ['>div']: { + display: 'flex', + alignItems: 'center', + gap: '10px', + + ['>span']: { + ...theme.font.body14Medium, + color: theme.colors.sub_gray5, + whiteSpace: 'nowrap', + ['&.condition']: { + width: '72px', + }, + }, + }, + }, +}); + +const Content = styled.div({ + display: 'flex', + flexDirection: 'column', + padding: '8px 10px 12px', + margin: '0 20px', + background: theme.colors.sub_gray11, + borderRadius: '4px', + gap: '20px', + + ['>span.divider']: { + width: '100%', + height: '1px', + background: theme.colors.sub_gray10, + }, +}); + +const patternReads = [ + { + key: 'roi', + text: '수익률', + items: [ + { key: 'success', condition: '0점 위쪽', text: '성공 🤗' }, + { key: 'failure', condition: '0점 아래쪽', text: '실패 😭' }, + ], + }, + { + key: 'score', + text: '인간지표', + items: [ + { key: 'success', condition: '50점 왼쪽', text: '성공 🤗' }, + { key: 'failure', condition: '50점 오른쪽', text: '실패 😭' }, + ], + }, +]; + +const AboutReportPattern = () => { + return ( + +
    +

    + 각 사분면은 무슨 패턴이에요? +

    + + + {patternReads.map((e1) => ( + + {e1.text} +
    + {e1.items.map((e2) => ( +
    + {e2.condition} + → 실험 {e2.text} +
    + ))} +
    +
    + ))} +
    +
    + + + +
    + ); +}; + +export default AboutReportPattern; diff --git a/src/components/Modal/AboutReportPattern/useAboutReportPattern.ts b/src/components/Modal/AboutReportPattern/useAboutReportPattern.ts new file mode 100644 index 00000000..e406d4c5 --- /dev/null +++ b/src/components/Modal/AboutReportPattern/useAboutReportPattern.ts @@ -0,0 +1,20 @@ +import BottomUpCancel from '../Layout/BottomUpCancel/BottomUpCancel'; +import useModal from '../useModal'; +import AboutReportPattern from './AboutReportPattern'; + +const useAboutReportPattern = (): { + openModal: () => void; + closeModal: () => void; + Modal: JSX.Element | null; +} => { + const { Modal, openModal, closeModal } = useModal({ + Layout: BottomUpCancel, + Component: AboutReportPattern, + modalKey: 'aboutReportPattern', + showDelay: 200, + }); + + return { Modal, openModal, closeModal }; +}; + +export default useAboutReportPattern; diff --git a/src/components/Modal/CenterTutorial/AboutAntVoice/AboutAntVoice.tsx b/src/components/Modal/CenterTutorial/AboutAntVoice/AboutAntVoice.tsx new file mode 100644 index 00000000..41af8d7c --- /dev/null +++ b/src/components/Modal/CenterTutorial/AboutAntVoice/AboutAntVoice.tsx @@ -0,0 +1,40 @@ +import styled from '@emotion/styled'; +import { theme } from '@styles/themes'; +import AntVoicePNG from '@assets/design/antVoice.png'; +import { ModalContainer, ModalContent, ModalTitleContainer } from '../CenterTutotial.Style'; + +const AntVoiceImage = styled.img({ + width: 'auto', + objectFit: 'cover', + margin: '0 16px', + boxSizing: 'border-box', +}); + +const AntVoiceDescription = styled.div({ + ...theme.font.detail12Semibold, + color: theme.colors.sub_gray11, + padding: '16px 12px', + background: theme.colors.sub_white, + borderRadius: '8px', + margin: '0 16px', + wordBreak: 'keep-all', +}); + +const AboutAntVoice = () => { + return ( + + +

    자주 언급되는 단어란?

    +
    + + + + 각종 커뮤니티 댓글을 한눈에 볼 수 있는 워드클라우드입니다. 크기가 클수록 각종 커뮤니티에서 가장 많이 언급된 + 단어입니다. + + +
    + ); +}; + +export default AboutAntVoice; diff --git a/src/components/Modal/CenterTutorial/AboutAntVoice/useAboutAntVoice.tsx b/src/components/Modal/CenterTutorial/AboutAntVoice/useAboutAntVoice.tsx new file mode 100644 index 00000000..e136de2c --- /dev/null +++ b/src/components/Modal/CenterTutorial/AboutAntVoice/useAboutAntVoice.tsx @@ -0,0 +1,20 @@ +import useModal from '@components/Modal/useModal'; +import CenterTutorialLayout from '../Layout'; +import AboutAntVoice from './AboutAntVoice'; + +const useAboutAntVoice = (): { + openModal: () => void; + closeModal: () => void; + Modal: JSX.Element | null; +} => { + const { Modal, openModal, closeModal } = useModal({ + Layout: CenterTutorialLayout, + Component: AboutAntVoice, + modalKey: 'aboutAntVoice', + showDelay: 100, + }); + + return { Modal, openModal, closeModal }; +}; + +export default useAboutAntVoice; diff --git a/src/components/Modal/CenterTutorial/AboutHumanZipyo/AboutHumanZipyo.tsx b/src/components/Modal/CenterTutorial/AboutHumanZipyo/AboutHumanZipyo.tsx new file mode 100644 index 00000000..341faf59 --- /dev/null +++ b/src/components/Modal/CenterTutorial/AboutHumanZipyo/AboutHumanZipyo.tsx @@ -0,0 +1,213 @@ +import styled from '@emotion/styled'; +import { useNavigate } from 'react-router-dom'; +import { webPath } from '@router/index'; +import GuageChart from '@components/Search/GuageChart/GuageChart'; +import { theme } from '@styles/themes'; +import LogoSVG from '@assets/logo_blue.svg?react'; +import { ModalContainer, ModalContent, ModalTitleContainer } from '../CenterTutotial.Style'; + +const HumanZipyoDescription = styled.div({ + ...theme.font.body14Semibold, + color: theme.colors.sub_gray10, + margin: '0 16px', + wordBreak: 'keep-all', +}); + +const HumanZipyoGuageChart = styled.div({ + gap: '8px', + display: 'flex', + flexDirection: 'column', + boxSizing: 'border-box', + justifyContent: 'center', + alignItems: 'center', + overflow: 'hidden', + + ['>div']: { + ['&.guage-chart']: { + width: '100%', + left: '50%', + marginTop: '-5%', + }, + + ['&.score-text']: { + display: 'flex', + padding: '0px 28px', + width: '100%', + boxSizing: 'border-box', + }, + + ['&.score-range']: { + display: 'flex', + gap: '4px', + padding: '0px 24px', + width: '100%', + boxSizing: 'border-box', + }, + }, +}); + +const HumanZipyoScoreText = styled.div( + ({ index }: { index: number }) => { + const backgroundColor = ['#11193E', '#121C46', '#141F53', '#1F359B', '#304CD1'][index]; + + return { + background: backgroundColor, + }; + }, + { + ...theme.font.detail10Medium, + color: theme.colors.sub_gray2, + textAlign: 'center', + width: '100%', + margin: '0px', + whiteSpace: 'nowrap', + minWidth: '0', + + ['@media (max-width: 360px)']: { + fontSize: '8px', + }, + }, +); + +const HumanZipyoScoreRange = styled.span({ + ...theme.font.detail10Medium, + color: theme.colors.sub_gray9, + textAlign: 'center', + width: '100%', + margin: '0px', + background: theme.colors.sub_white, + borderRadius: '2px', + position: 'relative', + whiteSpace: 'nowrap', + + ['::before']: { + content: '""', + position: 'absolute', + width: 0, + height: 0, + bottom: '100%', + left: '50%', + transform: 'translateX(-50%)', + + borderStyle: 'solid', + borderWidth: '0px 4px 6px 4px', + borderColor: `transparent transparent ${theme.colors.sub_white} transparent `, + }, +}); + +const HumanZipyoHowToContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '8px', + padding: '16px 12px', + margin: '0px 16px', + background: theme.colors.sub_white, + borderRadius: '8px', + alignItems: 'flex-start', + + ['>div']: { + display: 'flex', + flexDirection: 'column', + gap: '4px', + + ['>p']: { + margin: '0px', + + ['&.title']: { + ...theme.font.detail12Semibold, + color: theme.colors.sub_gray11, + }, + + ['&.description']: { + ...theme.font.detail12Medium, + color: theme.colors.sub_gray10, + }, + }, + }, + + ['>button']: { + background: theme.colors.sub_blue5, + padding: '6px 10px', + borderRadius: '4px', + border: 'none', + + ...theme.font.detail12Semibold, + color: theme.colors.sub_gray1, + }, +}); + +const HumanZipyoSubText = styled.p({ + ...theme.font.detail12Semibold, + color: theme.colors.sub_blue7, + + margin: '0 16px', + width: '100%', + boxSizing: 'border-box', + whiteSpace: 'nowrap', +}); + +const scoreText = ['대곰탕', '곰탕', '어?', '호황', '대호황']; +const scoreRange = [ + [0, 30], + [30, 40], + [40, 50], + [50, 70], + [70, 100], +]; + +const AboutHumanZipyo = () => { + const navigate = useNavigate(); + + const handleClickAbout = () => { + navigate(webPath.about()); + }; + + return ( + + + +

    점수란?

    +
    + + + 인간지표는 개미들의 ‘민심 온도계’예요. + 점수는 총 5단계로, 높을수록 시장 분위기가 들떠 있거나 + 과열된 상태를 뜻해요. + + +
    + +
    +
    + {scoreText.map((e, index) => ( + + LV{index + 1}. {e} + + ))} +
    +
    + {scoreRange.map((e, index) => { + return ( + + {e[0]}~{e[1]}점 + + ); + })} +
    +
    + +
    +

    점수는 어떻게 산출되나요?

    +

    + 대규모 감정분석 모델을 통해 각종 커뮤니티에서 투자자들 반응을 긍/부정으로 파악하여 점수를 산출해요 +

    +
    + +
    + *공식 지표가 아니므로 참고 용도로 활용해 주세요 +
    +
    + ); +}; + +export default AboutHumanZipyo; diff --git a/src/components/Modal/CenterTutorial/AboutHumanZipyo/useAboutHumanZipyo.tsx b/src/components/Modal/CenterTutorial/AboutHumanZipyo/useAboutHumanZipyo.tsx new file mode 100644 index 00000000..16dc8a1e --- /dev/null +++ b/src/components/Modal/CenterTutorial/AboutHumanZipyo/useAboutHumanZipyo.tsx @@ -0,0 +1,20 @@ +import useModal from '@components/Modal/useModal'; +import CenterTutorialLayout from '../Layout'; +import AboutHumanZipyo from './AboutHumanZipyo'; + +const useAboutHumanZipyo = (): { + openModal: () => void; + closeModal: () => void; + Modal: JSX.Element | null; +} => { + const { Modal, openModal, closeModal } = useModal({ + Layout: CenterTutorialLayout, + Component: AboutHumanZipyo, + modalKey: 'aboutHumanZipyo', + showDelay: 100, + }); + + return { Modal, openModal, closeModal }; +}; + +export default useAboutHumanZipyo; diff --git a/src/components/Modal/CenterTutorial/CenterTutotial.Style.ts b/src/components/Modal/CenterTutorial/CenterTutotial.Style.ts new file mode 100644 index 00000000..a42685cf --- /dev/null +++ b/src/components/Modal/CenterTutorial/CenterTutotial.Style.ts @@ -0,0 +1,84 @@ +import styled from '@emotion/styled'; +import { theme } from '@styles/themes'; + +const ModalLayout = styled.div( + ({ isShowModal, showDelay }: { isShowModal: boolean; showDelay: number }) => ({ + background: isShowModal ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0)', + backdropFilter: isShowModal ? 'blur(2px)' : '', + transition: `all ${showDelay}ms ease-in-out`, + + ['>div']: { + opacity: isShowModal ? 1 : 0, + transition: `all ${showDelay}ms ease-in-out`, + }, + }), + { + position: 'fixed', + width: '100%', + height: '100%', + top: 0, + left: 0, + right: 0, + bottom: 0, + zIndex: '100', + + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + padding: '30px', + boxSizing: 'border-box', + + ['>div']: { + background: theme.colors.sub_gray4, + display: 'flex', + flexDirection: 'column', + borderRadius: '8px', + overflow: 'hidden', + }, + }, +); + +const ModalContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '10px', + overflow: 'hidden auto', + maxHeight: '100%', + height: '100%', + padding: '20px 0', + overscrollBehavior: 'contain', +}); + +const ModalTitleContainer = styled.div({ + display: 'flex', + padding: '0 16px', + gap: '4px', + + ['>svg']: { + width: '72px', + height: 'auto', + }, + + ['>p']: { + margin: '0', + ...theme.font.body18Semibold, + color: theme.colors.sub_black, + }, +}); + +const ModalContent = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '10px', +}); + +const ModalCloseButton = styled.button({ + ...theme.font.body18Semibold, + color: theme.colors.sub_gray3, + background: theme.colors.sub_blue6, + border: 'none', + padding: '10px 0px', + width: '100%', +}); + +export { ModalLayout, ModalContainer, ModalTitleContainer, ModalContent, ModalCloseButton }; diff --git a/src/components/Modal/CenterTutorial/Layout.tsx b/src/components/Modal/CenterTutorial/Layout.tsx new file mode 100644 index 00000000..899c3c3e --- /dev/null +++ b/src/components/Modal/CenterTutorial/Layout.tsx @@ -0,0 +1,22 @@ +import { ModalLayoutProps } from '@components/Modal/useModal'; +import { ModalCloseButton, ModalLayout } from './CenterTutotial.Style'; + +const CenterTutorialLayout = ({ + children, + isShowModal, + modalRef, + handleClickOutSide, + closeModal, + showDelay, +}: ModalLayoutProps) => { + return ( + +
    + {children} + 이해했어요 +
    +
    + ); +}; + +export default CenterTutorialLayout; diff --git a/src/components/Modal/Common.Style.ts b/src/components/Modal/Common.Style.ts new file mode 100644 index 00000000..3257bed9 --- /dev/null +++ b/src/components/Modal/Common.Style.ts @@ -0,0 +1,71 @@ +import styled from "@emotion/styled"; +import { theme } from "@styles/themes"; + +export const Modal = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +`; + +export const ModalOverlay = styled.div` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); +`; + +export const ModalContent = styled.div` + background: ${theme.colors.sub_white}; + border-radius: 20px; + padding: 24px; + margin: 20px; + max-width: 324px; + width: 100%; + position: relative; + z-index: 1001; +`; + +export const ModalTitle = styled.h3` + ${theme.font.title20Semibold}; + color: ${theme.colors.sub_black}; + margin: 0 0 12px 0; + text-align: left; +`; + +export const ModalDescription = styled.p` + ${theme.font.body16Medium}; + color: ${theme.colors.sub_gray7}; + margin: 0 0 24px 0; + text-align: left; + line-height: 1.4; +`; + +export const ModalButtons = styled.div` + display: flex; + gap: 12px; +`; + +export const ModalButton = styled.button` + flex: 1; + background: ${theme.colors.sub_gray2}; + color: ${theme.colors.sub_gray8}; + border: none; + padding: 12px 0; + border-radius: 500px; + font-size: 18px; + font-weight: 700; + cursor: pointer; +`; + +export const ModalButtonPrimary = styled(ModalButton)` + background: ${theme.colors.sub_black}; + color: ${theme.colors.sub_white}; +`; diff --git a/src/components/Modal/Confirm/ConfirmModal.tsx b/src/components/Modal/Confirm/ConfirmModal.tsx new file mode 100644 index 00000000..d40a75f2 --- /dev/null +++ b/src/components/Modal/Confirm/ConfirmModal.tsx @@ -0,0 +1,139 @@ +import styled from '@emotion/styled'; +import { useEffect, useRef, useState } from 'react'; +import { theme } from '@styles/themes'; + +const ConfirmModalLayout = styled.div({ + display: 'flex', + position: 'fixed', + background: 'rgba(0, 0, 0, 0.7)', + width: '100%', + height: '100%', + zIndex: '100', + top: '0', + left: '50%', + transform: 'translateX(-50%)', + alignItems: 'center', + justifyContent: 'center', + padding: '32px', + boxSizing: 'border-box', + backdropFilter: 'blur(5px)', + maxWidth: '1280px', +}); + +const ConfirmModalContainer = styled.div({ + background: 'white', + display: 'flex', + flexDirection: 'column', + width: '100%', + borderRadius: '20px', + padding: '24px 20px 16px', + gap: '28px', + + ['>div']: { + display: 'flex', + gap: '12px', + + ['>button']: { + ...theme.font.body18Semibold, + width: '100%', + height: '48px', + padding: '8px 0', + border: 'none', + borderRadius: '999px', + + [':first-of-type']: { + background: '#E9ECEF', + color: '#495057', + }, + [':last-of-type']: { + background: '#1B1C1E', + color: '#E9ECEF', + }, + }, + }, +}); + +const ConfirmModalTextContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '8px', + + ['>p']: { + margin: '0', + wordBreak: 'keep-all', + + ['&.title']: { + ...theme.font.title20Semibold, + color: theme.colors.sub_gray8, + }, + + ['&.desc']: { + ...theme.font.body16Medium, + color: theme.colors.sub_gray7, + whiteSpace: 'pre-wrap', + wordBreak: 'keep-all', + }, + }, +}); + +const ConfirmModal = ({ + title, + description, + onConfirm, + isInverse, + actionText = ['네', '아니오'], +}: { + title: string; + description?: string | React.ReactNode; + onConfirm: () => void; + isInverse?: boolean; + actionText?: string[]; +}): [() => React.ReactElement, () => void, () => void] => { + const [isOpen, setIsOpen] = useState(false); + + const modalRef = useRef(null); + + const openModal = () => { + setIsOpen(true); + }; + const closeModal = () => { + setIsOpen(false); + }; + + useEffect(() => { + const handleClick = (e: MouseEvent) => { + if (modalRef.current && !modalRef.current.contains(e.target as Node)) { + closeModal(); + } + }; + + window.addEventListener('mousedown', handleClick); + + return () => { + window.removeEventListener('mousedown', handleClick); + }; + }, []); + + const modal = () => { + if (!isOpen) return <>; + + return ( + + + +

    {title}

    + {description &&

    {description}

    } +
    +
    + + +
    +
    +
    + ); + }; + + return [modal, openModal, closeModal]; +}; + +export default ConfirmModal; diff --git a/src/components/Modal/ExperimentDetail/ExperimentDetail.Style.ts b/src/components/Modal/ExperimentDetail/ExperimentDetail.Style.ts new file mode 100644 index 00000000..a85460f5 --- /dev/null +++ b/src/components/Modal/ExperimentDetail/ExperimentDetail.Style.ts @@ -0,0 +1,290 @@ +import styled from '@emotion/styled'; +import { deltaScoreToColor } from '@utils/ScoreConvert'; +import { theme } from '@styles/themes'; + +const ExperimentDetailContent = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '16px', + padding: '0 20px', +}); + +const RecortSheetTitleContainer = styled.div({ + display: 'flex', + alignItems: 'center', + gap: '12px', + padding: '14px 10px', + borderBottom: `1px solid ${theme.colors.sub_gray10}`, + + ['>img']: { + width: '32px', + height: '32px', + borderRadius: '999px', + }, + + ['>p']: { + margin: '0', + ...theme.font.body18Semibold, + color: theme.colors.sub_gray1, + }, +}); + +const ExperimentDetailIndexListContainer = styled.div({ + display: 'flex', +}); + +const ExperimentDetailIndexItemContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + flexGrow: '1', + + ['p']: { + margin: '0', + }, + + ['>p.title']: { + ...theme.font.body14Medium, + color: theme.colors.sub_gray6, + }, + + ['>div']: { + height: '52px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + + ['>p.value']: { + ...theme.font.body14Semibold, + color: theme.colors.sub_white, + }, + + ['>p.subValue']: { + ...theme.font.detail12Medium, + color: theme.colors.sub_gray6, + + ['&.white']: { + color: theme.colors.sub_gray2, + }, + }, + + ['>span']: { + ...theme.font.body14Semibold, + }, + + ['&.roi']: { + background: theme.colors.sub_gray11, + border: `1px solid ${theme.colors.sub_gray9}`, + borderRadius: '4px', + width: '100%', + }, + }, +}); + +// Chart + +const ExperimentDetailChartContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '8px', + width: '100%', + padding: '8px 10px 22px', + background: theme.colors.sub_gray11, + boxSizing: 'border-box', +}); + +const ExperimentDetailChartGraphContainer = styled.div({ + position: 'relative', + flexGrow: 1, + height: '200px', + + ['>canvas']: { + width: '100%', + height: '100%', + }, +}); + +const ExperimentDetailChartLayer = styled.div({ + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + display: 'flex', + justifyContent: 'space-around', +}); + +const ExperimentDetailChartDot = styled.div( + ({ score, enabled, selected }: { score: number | null; enabled: boolean; selected: boolean }) => ({ + ['>span']: { + display: score != undefined ? 'block' : 'none', + bottom: `${score ? score : 0}%`, + scale: enabled ? '1' : '0', + background: selected ? theme.colors.sub_blue6 : theme.colors.sub_gray11, + boxShadow: selected ? `0 0 0px 3px ${theme.colors.sub_blue6}33, 0 0 0px 9px ${theme.colors.sub_blue6}33` : 'none', + }, + }), + { + position: 'relative', + width: '20px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + margin: '20px 0px', + + ['>span']: { + position: 'absolute', + width: '12px', + height: '12px', + border: `2px solid ${theme.colors.sub_blue6}`, + boxSizing: 'border-box', + borderRadius: '50%', + transform: 'translateY(50%)', + transition: 'all 0.2s ease-in-out', + }, + }, +); + +const ExperimentDetailChartInfoLine = styled.div( + ({ index, score }: { index: number; score: number }) => { + const direction = score > 50 ? 'bottom' : 'top'; + + return { + left: `${10 + index * 20}%`, + ['>span']: { + top: direction == 'bottom' ? `calc(${100 - score}% + 6px)` : '45%', + bottom: direction == 'top' ? `calc(${score}% + 6px)` : '40%', + + backgroundImage: `linear-gradient(to ${direction}, #FFFFFF80 0px, #2947D2 100%)`, + maskImage: `repeating-linear-gradient(to ${direction}, #FFFFFF, #FFFFFF 3px, transparent 3px, transparent 6px)`, + webkitMaskImage: `repeating-linear-gradient(to ${direction}, #FFFFFF, #FFFFFF 3px, transparent 3px, transparent 6px)`, + }, + }; + }, + { + position: 'absolute', + top: '0', + bottom: '0', + margin: '20px 0px', + // background: 'rgba(255, 255, 255, 1)', + + ['>span']: { + position: 'absolute', + width: '2px', + transform: 'translateX(-50%)', + }, + }, +); + +const ExperimentDetailChartInfoContent = styled.div( + ({ index, score }: { index: number; score: number }) => { + const left = index == 0 ? '0' : index == 1 ? '20%' : index == 2 ? '50%' : 'auto'; + const right = index == 4 ? '0' : index == 3 ? '20%' : 'auto'; + const transform = `translateX(${index == 4 ? '0%' : index == 3 ? '50%' : index == 2 ? '-50%' : index == 1 ? '-50%' : '0%'})`; + const margin = `24px ${[1, 3].includes(index) ? '10%' : '0px'} 24px`; + + const direction = score > 50 ? 'bottom' : 'top'; + + return { + left, + right, + transform, + margin, + bottom: direction == 'bottom' ? '0' : 'auto', + top: direction == 'top' ? `0` : 'auto', + }; + }, + { + background: 'rgba(255, 255, 255, 0.12)', + border: '1px solid rgba(255, 255, 255, 0.1)', + backdropFilter: 'blur(10px)', + borderRadius: '999px', + padding: '10px 20px', + boxSizing: 'border-box', + position: 'absolute', + + ['>div']: { + display: 'flex', + flexDirection: 'column', + }, + + ['>p']: { + margin: '0', + ...theme.font.detail10Medium, + color: theme.colors.sub_gray5, + }, + }, +); + +const ExperimentDetailChartInfoItemContainer = styled.div( + ({ delta }: { delta: number }) => ({ + ['>p.diff']: { + color: deltaScoreToColor(delta) ?? theme.colors.sub_gray7, + }, + }), + { + display: 'flex', + alignItems: 'center', + gap: '4px', + + ['>p']: { + margin: '0', + whiteSpace: 'nowrap', + textAlign: 'left', + + ['&.name']: { + ...theme.font.detail10Bold, + color: theme.colors.sub_white, + width: '36px', + }, + ['&.value']: { + ...theme.font.detail10Medium, + color: theme.colors.sub_white, + }, + ['&.diff']: { + ...theme.font.detail10Medium, + }, + }, + + ['>span.divider']: { + width: '1px', + height: '10px', + background: theme.colors.sub_gray7, + }, + }, +); + +const ExperimentDetailChartDateContainer = styled.div({ + display: 'flex', + + ['>p']: { + ...theme.font.body14Medium, + color: theme.colors.sub_gray6, + width: '100%', + textAlign: 'center', + margin: '0', + }, +}); + +// + +const ColoredDiffLabel = styled.span(({ delta }: { delta: number }) => ({ + color: deltaScoreToColor(delta) ?? theme.colors.sub_gray7, +})); + +export { + ExperimentDetailContent, + RecortSheetTitleContainer, + ExperimentDetailIndexListContainer, + ExperimentDetailIndexItemContainer, + ExperimentDetailChartContainer, + ExperimentDetailChartGraphContainer, + ExperimentDetailChartLayer, + ExperimentDetailChartDot, + ExperimentDetailChartInfoLine, + ExperimentDetailChartInfoContent, + ExperimentDetailChartInfoItemContainer, + ExperimentDetailChartDateContainer, + ColoredDiffLabel, +}; diff --git a/src/components/Modal/ExperimentDetail/ExperimentDetail.tsx b/src/components/Modal/ExperimentDetail/ExperimentDetail.tsx new file mode 100644 index 00000000..f4774d3f --- /dev/null +++ b/src/components/Modal/ExperimentDetail/ExperimentDetail.tsx @@ -0,0 +1,339 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { STOCK_COUNTRY_MAP } from '@ts/StockCountry'; +import { webPath } from '@router/index'; +import StockImage from '@components/Common/StockImage'; +import { ExperimentDetailTradeInfo } from '@controllers/experiment/api'; +import { useExperimentDetailQuery } from '@controllers/experiment/query'; +import { theme } from '@styles/themes'; +import { + ColoredDiffLabel, + ExperimentDetailChartContainer, + ExperimentDetailChartDateContainer, + ExperimentDetailChartDot, + ExperimentDetailChartGraphContainer, + ExperimentDetailChartInfoContent, + ExperimentDetailChartInfoItemContainer, + ExperimentDetailChartInfoLine, + ExperimentDetailChartLayer, + ExperimentDetailContent, + ExperimentDetailIndexItemContainer, + ExperimentDetailIndexListContainer, + RecortSheetTitleContainer, +} from './ExperimentDetail.Style'; +import { ExperimentDetailModalData } from './useExperimentDetail'; + +// const DPR = window.devicePixelRatio; + +const getFormattedDate = (_date: Date | string) => { + const date = new Date(_date); + const [year, month, day] = [date.getFullYear(), date.getMonth() + 1, date.getDate()].map((num) => + num.toString().padStart(2, '0'), + ); + + return `${year}.${month}.${day}`; +}; + +export interface ExperimentDetailIndex { + key: string; + title: string; + value: string; + subValue?: string; +} + +const ExperimentDetail = ({ modalData: { experimentId } }: { modalData: ExperimentDetailModalData }) => { + const navigate = useNavigate(); + const { data: experimentDetail, isLoading } = useExperimentDetailQuery(experimentId); + + const indexList: ExperimentDetailIndex[] = useMemo(() => { + if (!experimentDetail) return []; + + const { buyAt, status, buyScore, buyPrice, currentScore, currentPrice, roi, country } = experimentDetail; + const currency = STOCK_COUNTRY_MAP[country].currency; + return [ + { + key: 'buyDate', + title: '매수일/상태', + value: `${getFormattedDate(buyAt!)}`, + subValue: `${status == 'PROGRESS' ? '실험중' : '실험완료'}`, + }, + { + key: 'buyTime', + title: '매수시점', + value: `${buyScore}점`, + subValue: `${currency}${buyPrice?.toLocaleString()}`, + }, + { + key: 'currentTime', + title: '현재시점', + value: `${currentScore}점`, + subValue: `${currency}${currentPrice?.toLocaleString()}`, + }, + { + key: 'roi', + title: '수익률', + value: `${!roi ? '' : roi > 0 ? '+' : ''}${(isNaN(roi!) ? 0 : roi!).toFixed(1)}%`, + subValue: `${roi}`, + }, + ]; + }, [experimentDetail]); + + const handleClickTitle = () => { + navigate(webPath.search(), { + state: { symbolName: experimentDetail?.symbolName, country: experimentDetail?.country }, + }); + }; + + if (isLoading) return
    Loading...
    ; + + if (!experimentDetail) return null; + + return ( + + + +

    {experimentDetail.symbolName}

    +
    + + {indexList.map((item) => ( + + ))} + + +
    + ); +}; + +const ExperimentDetailIndexItem = ({ indexItem }: { indexItem: ExperimentDetailIndex }) => { + const { key, title, value, subValue } = indexItem; + + return ( + +

    {title}

    +
    + {key != 'roi' ? ( + <> +

    {value}

    +

    {subValue}

    + + ) : ( + {value} + )} +
    +
    + ); +}; + +const ExperimentDetailChart = ({ + tradeInfos, + buyScore, + buyPrice, +}: { + tradeInfos: ExperimentDetailTradeInfo[]; + buyScore: number; + buyPrice: number; +}) => { + const chartCanvasRef = useRef(null); + + const loadChartStatusRef = useRef({ + time: 0, + isStart: false, + }); + const loadChartFrameRef = useRef(0); + + const [enableDots, setEnableDots] = useState(Array.from({ length: 5 }, () => false)); + const [selectedDot, setSelectedDot] = useState(-1); + + const drawChart = (noAnimation: boolean = false) => { + const canvas = chartCanvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const { width, height } = canvas.getBoundingClientRect(); + + const length = 5; + const fullTime = 200; + const delay = fullTime / (length - 1); + + const { time } = loadChartStatusRef.current; + const xScale = (length - 1) / length; + const yScale = 0.8; + + ctx.clearRect(0, 0, width, height); + ctx.strokeStyle = theme.colors.sub_blue6; + ctx.lineWidth = 2; + + const getScaledCoord = (x: number, y: number): [number, number] => { + return [x * xScale + ((1 - xScale) / 2) * width, y * yScale + ((1 - yScale) / 2) * height]; + }; + + tradeInfos.forEach((e, index, arr) => { + if (time < index * delay) return; + if (index == length - 1 || index == arr.length - 1) return; + + const indexTime = time - index * delay; + + const startX = index * (width / (length - 1)); + const startY = height - e.score * (height / 100); + + const endX = (index + 1) * (width / (length - 1)); + const endY = height - arr[index + 1].score * (height / 100); + const slope = (endY - startY) / (endX - startX); + const x = Math.min(indexTime * (width / fullTime), endX - startX); + const Y = slope * x; + + ctx.beginPath(); + ctx.moveTo(...getScaledCoord(startX, startY)); + ctx.lineTo(...getScaledCoord(startX + x, startY + Y)); + ctx.closePath(); + ctx.stroke(); + }); + + tradeInfos.forEach((_, index) => { + if (time < index * delay - delay / 2) return; + + setEnableDots((prev) => { + const newEnableDots = [...prev]; + newEnableDots[index] = true; + return newEnableDots; + }); + if (index == tradeInfos.length - 1) { + if (selectedDot == -1) { + setTimeout(() => setSelectedDot(index), 100); + } + } + }); + + if (loadChartStatusRef.current.time < fullTime + 10 && !noAnimation) { + loadChartStatusRef.current.time++; + loadChartFrameRef.current = requestAnimationFrame(() => drawChart()); + } else { + } + return; + }; + + const [canvasInfo, setCanvasInfo] = useState({ + width: 0, + height: 0, + }); + + useEffect(() => { + const canvas = chartCanvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const { width, height } = canvas.getBoundingClientRect(); + setCanvasInfo({ width, height }); + + if (loadChartStatusRef.current.isStart) return; + loadChartStatusRef.current.isStart = true; + loadChartFrameRef.current = requestAnimationFrame(() => drawChart()); + }, [tradeInfos]); + + useEffect(() => { + const container = chartCanvasRef.current; + if (!container) return; + + const resizeObserver = new ResizeObserver((entries) => { + const entry = entries[0]; + if (!entry) return; + + const { width, height } = entry.contentRect; + setCanvasInfo({ width, height }); + }); + + resizeObserver.observe(container); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + useEffect(() => { + drawChart(true); + }, [canvasInfo]); + + const selectedTradeInfo = selectedDot != -1 ? tradeInfos[selectedDot] : null; + const infoItemText = useMemo(() => { + if (!selectedTradeInfo) return null; + + const { score: currentScore, price: currentPrice } = selectedTradeInfo; + const scoreDiff = currentScore - buyScore; + const scoreDiffSign = !scoreDiff ? '' : scoreDiff > 0 ? '+' : '-'; + const roi = ((currentPrice - buyPrice) / buyPrice) * 100; + const roiDiff = roi; + const roiDiffSign = !roiDiff ? '' : roiDiff > 0 ? '+' : '-'; + + return selectedTradeInfo + ? [ + { + name: '인간지표', + value: `${currentScore}점`, + diff: `(${scoreDiffSign}${Math.abs(scoreDiff)}점)`, + delta: scoreDiff, + }, + { + name: '수익률', + value: `${roi.toFixed(1)}%`, + diff: `(${roiDiffSign}${Math.abs(roiDiff).toFixed(1)}%)`, + delta: roiDiff, + }, + ] + : null; + }, [selectedTradeInfo, buyScore, buyPrice]); + + return ( + + + + + {Array.from({ length: 5 }).map((_, index) => ( + + setSelectedDot((prev) => (prev == index ? -1 : index))} /> + + ))} + {infoItemText && ( + <> + + + + +
    + {infoItemText.map((e) => ( + +

    {e.name}

    + +

    {e.value}

    +

    {e.diff}

    +
    + ))} +
    +

    *()는 매수시점 대비

    +
    + + )} +
    +
    + + {Array.from({ length: 5 }).map((_, index) => ( +

    D-{5 - index}

    + ))} +
    +
    + ); +}; + +export default ExperimentDetail; diff --git a/src/components/Modal/ExperimentDetail/useExperimentDetail.ts b/src/components/Modal/ExperimentDetail/useExperimentDetail.ts new file mode 100644 index 00000000..35adf5df --- /dev/null +++ b/src/components/Modal/ExperimentDetail/useExperimentDetail.ts @@ -0,0 +1,24 @@ +import BottomUpCancel from '../Layout/BottomUpCancel/BottomUpCancel'; +import useModal from '../useModal'; +import ExperimentDetail from './ExperimentDetail'; + +export interface ExperimentDetailModalData { + experimentId: number; +} + +const useExperimentDetailModal = (): { + openModal: (modalData: ExperimentDetailModalData) => void; + closeModal: () => void; + Modal: JSX.Element | null; +} => { + const { Modal, openModal, closeModal } = useModal({ + Layout: BottomUpCancel, + Component: ExperimentDetail, + modalKey: 'experimentDetail', + showDelay: 200, + }); + + return { Modal, openModal, closeModal }; +}; + +export default useExperimentDetailModal; diff --git a/src/components/Modal/Layout/BottomUpCancel/BottomUpCancel.tsx b/src/components/Modal/Layout/BottomUpCancel/BottomUpCancel.tsx new file mode 100644 index 00000000..db45ee48 --- /dev/null +++ b/src/components/Modal/Layout/BottomUpCancel/BottomUpCancel.tsx @@ -0,0 +1,77 @@ +import styled from '@emotion/styled'; +import { ModalLayoutProps } from '@components/Modal/useModal'; +import { theme } from '@styles/themes'; + +const ModalLayout = styled.div( + ({ isShowModal, showDelay }: { isShowModal: boolean; showDelay: number }) => ({ + background: isShowModal ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0)', + backdropFilter: isShowModal ? 'blur(2px)' : '', + transition: `all ${showDelay}ms ease-in-out`, + + ['>div']: { + transform: `translateY(${isShowModal ? '0' : '100%'})`, + transition: `all ${showDelay}ms ease-in-out`, + }, + }), + { + position: 'fixed', + width: '100%', + height: '100%', + top: 0, + left: '50%', + transform: 'translateX(-50%)', + right: 0, + bottom: 0, + zIndex: '100', + overflow: 'auto', + maxWidth: '1280px', + + display: 'flex', + flexDirection: 'column', + justifyContent: 'flex-end', + + ['>div']: { + background: theme.colors.sub_black, + width: '100%', + border: `1px solid ${theme.colors.sub_gray10}`, + borderRadius: '16px 16px 0 0', + boxSizing: 'border-box', + padding: '30px 0px 60px', + gap: '72px', + display: 'flex', + flexDirection: 'column', + overflow: 'auto', + overscrollBehavior: 'contain', + }, + }, +); + +const ModalCancelButton = styled.button({ + ...theme.font.body18Semibold, + color: theme.colors.sub_white, + background: theme.colors.sub_blue6, + borderRadius: '8px', + border: 'none', + padding: '10px 0px', + margin: '0px 20px', +}); + +const BottomUpCancel = ({ + children, + isShowModal, + modalRef, + handleClickOutSide, + closeModal, + showDelay, +}: ModalLayoutProps) => { + return ( + +
    + {children} + 닫기 +
    +
    + ); +}; + +export default BottomUpCancel; diff --git a/src/components/Modal/useModal.tsx b/src/components/Modal/useModal.tsx new file mode 100644 index 00000000..dd566abf --- /dev/null +++ b/src/components/Modal/useModal.tsx @@ -0,0 +1,107 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +export interface ModalLayoutProps { + children?: React.ReactNode; + isShowModal: boolean; + modalRef: React.RefObject; + handleClickOutSide: (e: React.MouseEvent) => void; + closeModal: () => void; + showDelay: number; +} + +const useModal = ({ + Layout, + Component, + modalKey, + showDelay = 0, +}: { + Layout: ({ + children, + isShowModal, + modalRef, + handleClickOutSide, + closeModal, + showDelay, + }: ModalLayoutProps) => JSX.Element; + Component: ({ modalData }: { modalData: T }) => JSX.Element | null; + modalKey: string; + showDelay?: number; +}): { + openModal: (modalData: T) => void; + closeModal: () => void; + Modal: JSX.Element | null; +} => { + const location = useLocation(); + const navigate = useNavigate(); + + const { state = {} } = location; + + const modalRef = useRef(null); + const [isShowModal, setIsShowModal] = useState(false); + const showModalTimeoutRef = useRef(null); + + const openModal = (modalData: T) => { + localStorage.setItem('scrollPosition', window.scrollY.toString()); + navigate('', { + state: { + ...state, + [modalKey]: { + isOpen: true, + modalData: modalData, + }, + }, + }); + }; + + const closeModal = () => { + setIsShowModal(false); + showModalTimeoutRef.current = setTimeout(() => { + localStorage.setItem('scrollPosition', window.scrollY.toString()); + navigate(-1); + }, showDelay); + }; + + useEffect(() => { + const scrollPosition = localStorage.getItem('scrollPosition'); + if (scrollPosition) { + window.scrollTo(0, parseInt(scrollPosition)); + localStorage.removeItem('scrollPosition'); + } + + const { isOpen } = location?.state?.[modalKey] ?? {}; + showModalTimeoutRef.current = setTimeout(() => { + setIsShowModal(isOpen); + }, 0); + }, [location]); + + const handleClickOutSide = (e: React.MouseEvent) => { + if (modalRef.current && e.target == modalRef.current) { + closeModal(); + } + }; + + const Modal = useMemo(() => { + const { isOpen, modalData } = location?.state?.[modalKey] ?? {}; + if (!isOpen) return null; + return ( + + + + ); + }, [location, isShowModal]); + + return { + Modal, + openModal, + closeModal, + }; +}; + +export default useModal; diff --git a/src/components/MyPage/MyPage.Style.ts b/src/components/MyPage/MyPage.Style.ts new file mode 100644 index 00000000..139597f9 --- /dev/null +++ b/src/components/MyPage/MyPage.Style.ts @@ -0,0 +1,2 @@ + + diff --git a/src/components/MyPage/MyPageInput/MyPageInput.tsx b/src/components/MyPage/MyPageInput/MyPageInput.tsx new file mode 100644 index 00000000..110972be --- /dev/null +++ b/src/components/MyPage/MyPageInput/MyPageInput.tsx @@ -0,0 +1,106 @@ +import styled from '@emotion/styled'; +import { theme } from '@styles/themes'; + +const MyPageInputContainer = styled.div( + ({ isError }: { isError: boolean }) => ({ + ['>p']: { + color: isError ? theme.colors.sub_red : theme.colors.sub_gray3, + }, + + ['>input']: { + outline: isError ? `1px solid ${theme.colors.sub_red}` : 'none', + }, + }), + { + display: 'flex', + flexDirection: 'column', + gap: '8px', + padding: '0 20px', + + ['>p']: { + margin: '0', + ...theme.font.body16Medium, + }, + + ['>input']: { + border: 'none', + padding: '20px 16px', + height: '48px', + boxSizing: 'border-box', + borderRadius: '5px', + background: theme.colors.sub_gray11, + color: theme.colors.sub_gray3, + ...theme.font.body16Medium, + + ['&::placeholder']: { + color: theme.colors.sub_gray8, + }, + }, + }, +); + +const MyPageInputSubContainer = styled.div({ + display: 'flex', + justifyContent: 'space-between', + height: '20px', + + ['>p']: { + margin: '0', + + ['&.error']: { + padding: '0 4px', + ...theme.font.body14Medium, + color: theme.colors.sub_red, + }, + + ['&.sub']: { + ...theme.font.body14Regular, + color: theme.colors.sub_gray3, + + ['>span']: { + ...theme.font.body14Semibold, + }, + }, + }, +}); + +export interface MyPageInputProps { + name: string; + error: string; + title: string; + sub?: React.ReactElement; + inputs: { + key: string; + value: string; + placeholder: string; + disabled?: boolean; + handleChange: (e: React.ChangeEvent) => void; + }[]; +} + +const MyPageInput = (props: MyPageInputProps) => { + const { name, error, title, sub, inputs } = props; + + return ( + +

    {title}

    + {inputs.map((e, i) => ( + + ))} + +

    {error}

    + {!!sub &&

    {sub}

    } +
    +
    + ); +}; + +export default MyPageInput; diff --git a/src/components/MyPage/ProfileCircle/ProfileCircle.tsx b/src/components/MyPage/ProfileCircle/ProfileCircle.tsx new file mode 100644 index 00000000..0732259f --- /dev/null +++ b/src/components/MyPage/ProfileCircle/ProfileCircle.tsx @@ -0,0 +1,70 @@ +import styled from '@emotion/styled'; +import EditCircleSVG from '@assets/edit_circle.svg?react'; +import ProfilePNG from '@assets/profile.png'; + +const ProfileCircleContainer = styled.label( + ({ size }: { size: 'small' | 'medium' | 'large' }) => ({ + ['>img']: { + width: size === 'small' ? '48px' : size === 'medium' ? '64px' : '76px', + }, + + ['>svg']: { + width: size === 'small' ? '16px' : size === 'medium' ? '20px' : '24px', + }, + }), + { + display: 'flex', + position: 'relative', + + ['>img']: { + height: 'auto', + aspectRatio: '1 / 1', + objectFit: 'cover', + borderRadius: '999px', + flexShrink: '0', + }, + + ['>svg']: { + position: 'absolute', + bottom: '0', + right: '0', + height: 'auto', + aspectRatio: '1 / 1', + fill: '#ADB5BD', + background: '#495057', + borderRadius: '999px', + }, + + ['>input']: { + display: 'none', + }, + }, +); + +const ProfileCircle = ({ + profileImage, + handleChangeFile, + size, + canEdit = true, + handleClickCircle, +}: { + profileImage: string; + handleChangeFile: (e: React.ChangeEvent) => void; + size: 'small' | 'medium' | 'large'; + canEdit?: boolean; + handleClickCircle?: (e: React.MouseEvent) => void; +}) => { + return ( + + + {canEdit && ( + <> + + + + )} + + ); +}; + +export default ProfileCircle; diff --git a/src/components/NoLoginWrapper/NoLoginWrapper.style.ts b/src/components/NoLoginWrapper/NoLoginWrapper.style.ts new file mode 100644 index 00000000..443063d9 --- /dev/null +++ b/src/components/NoLoginWrapper/NoLoginWrapper.style.ts @@ -0,0 +1,88 @@ +import styled from '@emotion/styled'; +import { theme } from '@styles/themes'; + +export const Overlay = styled.div( + ({ hasHeader, hasNavbar }: { hasHeader?: boolean; hasNavbar?: boolean }) => ({ + top: hasHeader ? '60px' : 0, + bottom: hasNavbar ? '96px' : 0, + }), + { + position: 'fixed', + left: '50%', + transform: 'translateX(-50%)', + right: 0, + zIndex: 50, + backdropFilter: 'blur(5px)', + WebkitBackdropFilter: 'blur(5px)', + gap: '20px', + maxWidth: '1280px', + width: '100%', + background: 'linear-gradient(180deg, rgba(16, 16, 16, 0.4) 0%, #101010 44.56%)', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + + ['>div']: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + overflow: 'auto', + overscrollBehavior: 'contain', + padding: '100px 20px', + }, + }, +); + +export const TitleContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: '10px', + + ['>p']: { + margin: '0', + textAlign: 'center', + whiteSpace: 'pre-wrap', + + ['&.title']: { + ...theme.font.title20Semibold, + color: theme.colors.sub_gray2, + }, + + ['&.description']: { + ...theme.font.body14Medium, + color: theme.colors.sub_gray4, + }, + }, +}); + +export const ButtonContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: '20px', + width: '220px', + + ['>button']: { + ...theme.font.body18Semibold, + appearance: 'none', + border: 0, + padding: '10px 28px', + borderRadius: '999px', + boxSizing: 'border-box', + width: '100%', + + ['&.primary']: { + color: theme.colors.sub_gray11, + background: theme.colors.sub_white, + }, + + ['&.secondary']: { + color: theme.colors.sub_gray5, + background: theme.colors.sub_gray9, + }, + }, +}); diff --git a/src/components/NoLoginWrapper/NoLoginWrapper.tsx b/src/components/NoLoginWrapper/NoLoginWrapper.tsx new file mode 100644 index 00000000..a3bd5636 --- /dev/null +++ b/src/components/NoLoginWrapper/NoLoginWrapper.tsx @@ -0,0 +1,63 @@ +import { ReactNode } from 'react'; +import { useNavigate } from 'react-router-dom'; +import useAuthInfo from '@hooks/useAuthInfo'; +import { ButtonContainer, Overlay, TitleContainer } from './NoLoginWrapper.style'; + +export interface NoLoginWrapperProps { + title: ReactNode; + description: string | ReactNode; + buttonText: string; + children?: ReactNode; + className?: string; + SecondaryButtonText?: string; + hasHeader?: boolean; + hasNavbar?: boolean; +} + +const NoLoginWrapper = (props: NoLoginWrapperProps) => { + const { isLogin, handleNavigateLogin } = useAuthInfo(); + const navigate = useNavigate(); + + const { title, description, buttonText, children, className, SecondaryButtonText, hasHeader, hasNavbar } = props; + + const handleClick = () => { + handleNavigateLogin(); + }; + + const handleSecondaryClick = () => { + navigate('/'); + }; + + if (isLogin) return null; + + return ( + +
    + +

    {title}

    +

    {description}

    +
    + {children} + + + {SecondaryButtonText && ( + + )} + +
    +
    + ); +}; + +export default NoLoginWrapper; diff --git a/src/components/PWAUsage/Android/Android.tsx b/src/components/PWAUsage/Android/Android.tsx deleted file mode 100644 index 6fc407fa..00000000 --- a/src/components/PWAUsage/Android/Android.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import AddToHomeSVG from '@assets/PWA/Android/AddToHome.svg?react'; -import ShareButtonSVG from '@assets/PWA/Android/ShareButton.svg?react'; -import RunAppSVG from '@assets/PWA/RunApp.svg?react'; -import { - DetailContainer, - DetailItem, - DetailNumber, - DetailText, - HeaderText, - OrderContainer, -} from '../Common.style'; - -const IOS = () => ( - - - 홈화면에 앱을
    - 추가하세요. -
    - - -
    - 1 - chrome 접속, 상단 우측 버튼 탭 -
    - -
    - -
    - 2 - 홈 화면에 추가 -
    - -
    - -
    - 3 - 생성된 앱 실행 -
    - -
    -
    -
    -); - -export default IOS; diff --git a/src/components/PWAUsage/Common.style.ts b/src/components/PWAUsage/Common.style.ts deleted file mode 100644 index 90544e3d..00000000 --- a/src/components/PWAUsage/Common.style.ts +++ /dev/null @@ -1,76 +0,0 @@ -import styled from '@emotion/styled'; -import { theme } from '@styles/themes'; - -const OrderContainer = styled.div({ - boxSizing: 'border-box', - display: 'flex', - width: '100%', - - flexDirection: 'column', - alignItems: 'flex-start', - gap: '20px', - marginTop: '30px', -}); - -const HeaderText = styled.h2({ - color: theme.colors.primary0, - fontFamily: 'Pretendard', - fontSize: '25px', - fontStyle: 'normal', - fontWeight: 700, - lineHeight: '1.5', - textAlign: 'left', -}); - -const DetailContainer = styled.div({ - display: 'flex', - flexDirection: 'column', // 세로 정렬로 변경 - gap: '50px', // 각 항목 간 간격 - width: '100%', // 전체 너비 차지 -}); - -const DetailItem = styled.div({ - display: 'flex', - width: '100%', - flexDirection: 'column', - alignItems: 'flex-start', - gap: '22px', - - ['div']: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - gap: '12px', - }, - - ['svg']: { - alignItems: 'flex-start', - maxWidth: '100%', - }, -}); - -const DetailNumber = styled.div({ - width: '20px', - height: '20px', - fontSize: '16px', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - fontWeight: 700, - borderRadius: '4px', - background: theme.colors.primary50, - color: theme.colors.primary0, -}); - -const DetailText = styled.div({ - color: theme.colors.primary0, - fontFamily: 'Pretendard', - fontSize: '20px', - fontStyle: 'normal', - fontWeight: 700, - lineHeight: '1.5', - textAlign: 'left', - flex: 1, // 텍스트 영역 확장 -}); - -export { OrderContainer, HeaderText, DetailContainer, DetailItem, DetailNumber, DetailText }; diff --git a/src/components/PWAUsage/iOS/IOS.style.ts b/src/components/PWAUsage/iOS/IOS.style.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/PWAUsage/iOS/IOS.tsx b/src/components/PWAUsage/iOS/IOS.tsx deleted file mode 100644 index b16afec6..00000000 --- a/src/components/PWAUsage/iOS/IOS.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import AddToHomeSVG from '@assets/PWA/IOS/AddToHome.svg?react'; -import ShareButtonSVG from '@assets/PWA/IOS/ShareButton.svg?react'; -import RunAppSVG from '@assets/PWA/RunApp.svg?react'; -import { - DetailContainer, - DetailItem, - DetailNumber, - DetailText, - HeaderText, - OrderContainer, -} from '../Common.style'; - -const IOS = () => ( - - - 홈화면에 앱을
    - 추가하세요. -
    - - -
    - 1 - safari 접속, 하단 공유 버튼 탭 -
    - -
    - -
    - 2 - 홈 화면에 추가 -
    - -
    - -
    - 3 - 생성된 앱 실행 -
    - -
    -
    -
    -); - -export default IOS; diff --git a/src/components/PopUp/AntiVoicePopUp/AntVoicePopUp.style.ts b/src/components/PopUp/AntiVoicePopUp/AntVoicePopUp.style.ts deleted file mode 100644 index 1403f4cd..00000000 --- a/src/components/PopUp/AntiVoicePopUp/AntVoicePopUp.style.ts +++ /dev/null @@ -1,115 +0,0 @@ -import styled from '@emotion/styled'; -import { Globals } from '@components/Common/Common.Type'; -import { media, theme, themeColor } from '@styles/themes'; - -const PopUpImage = styled('ul')({ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - padding: 0, - listStyle: 'none', - - ['div']: { - display: 'flex', - flexDirection: 'column', - alignItems: 'left', - justifyContent: 'space-between', - gap: '8px', - flex: 1, - height: '160px', - }, - [media[0]]: { - ['div']: { - height: '120px', - }, - }, -}); - -const PopUpDetailWord = styled.p( - ({ - color, - fontSize, - textAlign, - }: { - color?: themeColor; - fontSize?: number; - textAlign?: - | Globals - | '-webkit-match-parent' - | 'center' - | 'end' - | 'justify' - | 'left' - | 'match-parent' - | 'right' - | 'start'; - }) => ({ - textAlign: textAlign || 'left', - fontWeight: '700', - fontSize: fontSize ? `${fontSize}px` : '36px', - margin: 0, - padding: 0, - background: theme.colors.grayscale90, - color: color ? theme.colors[color] : theme.colors.primary0, // 색상이 없는 경우 기본값 사용 - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: '100%', - width: '100%', - lineHeight: 1.3, - [media[0]]: { - fontSize: fontSize ? `${(fontSize * 2) / 3}px` : '24px', - }, - }), -); - -const PopUpDetailContainer = styled('div')({ - display: 'flex', - flexDirection: 'column', - gap: '12px', - background: theme.colors.grayscale10, - borderRadius: '8px', - padding: '16px', - marginTop: '12px', - - [media[0]]: { - padding: '12px', - marginTop: '8px', - }, -}); - -const PopUpDetail = styled('div')({ - display: 'flex', - alignItems: 'center', - gap: '12px', - - ['span']: { - fontSize: '14px', - color: theme.colors.grayscale100, - }, - - [media[0]]: { - gap: 'px', - }, -}); - -const PopUpDetailNumber = styled.div(({ color }: { color?: themeColor }) => ({ - width: '24px', - height: '24px', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - fontSize: '14px', - fontWeight: '700', - borderRadius: '4px', - background: color ? theme.colors[color] : theme.colors.primary40, - color: theme.colors.primary0, - - [media[0]]: { - width: '20px', // 모바일 크기 축소 - height: '20px', - fontSize: '12px', - }, -})); - -export { PopUpImage, PopUpDetailWord, PopUpDetailContainer, PopUpDetail, PopUpDetailNumber }; diff --git a/src/components/PopUp/AntiVoicePopUp/AntVoicePopUp.tsx b/src/components/PopUp/AntiVoicePopUp/AntVoicePopUp.tsx deleted file mode 100644 index d6b8a62e..00000000 --- a/src/components/PopUp/AntiVoicePopUp/AntVoicePopUp.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import CommonPopUp from '../CommonPopUp'; -import { PopUpContent, PopUpTitle } from '../CommonPopUp.style'; -import { - PopUpDetail, - PopUpDetailContainer, - PopUpDetailNumber, - PopUpDetailWord, - PopUpImage, -} from './AntVoicePopUp.style'; - -const AntiVoicePopUp = ({ onClose }: { onClose: () => void }) => ( - - 개미들의 목소리란? - - 각종 커뮤니티의 댓글을 한눈에 볼 수 있는 워드클라우드에요. - -
    - 1 - - 가장 많이 -
    언급된 단어 -
    -
    - -
    - 2 - - 상대적으로 적게 -
    언급된 단어 -
    -
    -
    - - - 1 - - 크기가 클수록 각종 커뮤니티에서 -
    가장 많이 언급된 단어예요. -
    -
    - - 2 - - 크기가 작을수록 각종 커뮤니티에서 -
    상대적으로 적게 언급된 단어에요 -
    -
    -
    -
    -
    -); - -export default AntiVoicePopUp; diff --git a/src/components/PopUp/CommonPopUp.style.ts b/src/components/PopUp/CommonPopUp.style.ts index 92649e58..0fe93b70 100644 --- a/src/components/PopUp/CommonPopUp.style.ts +++ b/src/components/PopUp/CommonPopUp.style.ts @@ -2,57 +2,30 @@ import styled from '@emotion/styled'; import { media, theme } from '@styles/themes'; const PopUpContainer = styled('div')({ + position: 'relative', display: 'flex', flexDirection: 'column', - position: 'fixed', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: '568px', + width: '100%', height: 'auto', background: theme.colors.grayscale30, color: theme.colors.primary100, - borderRadius: '12px', + borderRadius: '8px', boxShadow: '0 4px 10px rgba(0, 0, 0, 0.2)', - zIndex: 1000, fontFamily: 'Pretendard', + overflow: 'hidden', [media[0]]: { - width: '90%', ['svg']: { width: '40%', }, }, }); -const PopUpTitle = styled('div')({ - display: 'flex', - alignItems: 'center', - gap: '8px', - fontSize: '36px', - fontWeight: '700', - padding: '48px 32px 0 32px', - - ['svg']: { - height: '36px', - width: 'auto', - }, - - [media[0]]: { - fontSize: '20px', // 모바일에서는 작은 글자 크기 - padding: '32px 32px 0 32px', - ['svg']: { - height: '18px', - width: 'auto', - }, - }, -}); - const PopUpContent = styled('div')({ - padding: '0 32px 32px 32px', + padding: '20px 16px', display: 'flex', flexDirection: 'column', - gap: '16px', + gap: '10px', fontFamily: 'Pretendard', fontSize: '16px', fontStyle: 'normal', @@ -62,26 +35,32 @@ const PopUpContent = styled('div')({ color: theme.colors.grayscale100, }); -const StyledSpan = styled('span')({ - color: theme.colors.primary50, - fontWeight: '700', +const PopUpTitle = styled('div')({ + display: 'flex', + alignItems: 'center', + gap: '4px', + + ...theme.font.body18Semibold, + color: theme.colors.primary100, + + ['>svg']: { + width: '72px', + height: 'auto', + }, }); const ConfirmButton = styled('div')({ textAlign: 'center', - fontWeight: '700', - lineHeight: '1.5', - fontSize: '24px', cursor: 'pointer', - background: theme.colors.primary50, + background: theme.colors.sub_blue6, color: theme.colors.primary0, - borderRadius: '0 0 12px 12px', - padding: '27px 0', + padding: '12px 0', + ...theme.font.body18Semibold, +}); - [media[0]]: { - fontSize: '16px', // 모바일 글자 크기 축소 - padding: '16px 0', // 모바일 패딩 축소 - }, +const StyledSpan = styled('span')({ + color: theme.colors.primary50, + fontWeight: '700', }); const Backdrop = styled('div')({ @@ -105,8 +84,8 @@ const CloseButton = styled('button')({ color: theme.colors.grayscale100, [media[0]]: { - top: '12px', - right: '0px', + top: '8px', + right: '8px', }, }); diff --git a/src/components/PopUp/CommonPopUp.tsx b/src/components/PopUp/CommonPopUp.tsx index 25faad7b..e990730b 100644 --- a/src/components/PopUp/CommonPopUp.tsx +++ b/src/components/PopUp/CommonPopUp.tsx @@ -1,14 +1,10 @@ -import { Backdrop, CloseButton, ConfirmButton, PopUpContainer } from './CommonPopUp.style'; +import { ConfirmButton, PopUpContainer, PopUpContent } from './CommonPopUp.style'; const CommonPopUp = ({ children, onClose }: { children: any; onClose: () => void }) => ( - <> - - - - {children} - 이해했어요 - - + + {children} + 이해했어요 + ); export default CommonPopUp; diff --git a/src/components/PopUp/PWAinfoPopUp/PWAInfoPopUp.tsx b/src/components/PopUp/PWAinfoPopUp/PWAInfoPopUp.tsx index 391bda51..1f76b74e 100644 --- a/src/components/PopUp/PWAinfoPopUp/PWAInfoPopUp.tsx +++ b/src/components/PopUp/PWAinfoPopUp/PWAInfoPopUp.tsx @@ -1,51 +1,39 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useIsMobile } from '@hooks/useIsMobile'; +import useLocalStorageState from '@hooks/useLocalStorageState'; import { webPath } from '@router/index'; -import { ImgDiv } from '@components/Common/Common'; import PWAPNG from '@assets/PWA/PWA.png'; +import CrossSVG from '@assets/icons/cross.svg?react'; import { Backdrop, - ButtonContainer, - Close24HourButton, - CloseButton, - ConfirmButton, - DetailContainer, - HeaderText, - NormalText, + PWAInfoButtonContainer, PWAInfoContainer, - TextArea, + PWAInfoContents, + PWAInfoTextContainer, } from './PWAinfoPopUp.style'; const PWAInfoPopUp = ({}: {}) => { - const [showPopUp, setShowPopUp] = useState(false); - const isMobile = useIsMobile(); const navigate = useNavigate(); - - useEffect(() => { - const today = new Date(); - const VISITED = localStorage.getItem('LAST_VISIT_POPUP'); // 마지막 방문 시간을 로컬 스토리지에서 가져옴 - - const handleMainPop = () => { - if (VISITED) { - const lastVisit = new Date(VISITED); - const diff = today.getTime() - lastVisit.getTime(); - const diffHours = diff / (1000 * 60 * 60); - - if (diffHours < 24) { - return; - } + const isMobile = useIsMobile(); + const [lastVisit, setLastVisit] = useLocalStorageState('last_visit_page'); + const [showPopUp, setShowPopUp] = useState( + (() => { + if (!lastVisit) { + return true; } - setShowPopUp(true); - }; + const diff = new Date().getTime() - new Date(lastVisit).getTime(); + if (diff < 1000 * 60 * 60 * 24) { + return false; + } - handleMainPop(); - }, []); + return true; + })(), + ); const closePopUp24Hours = () => { - const today = new Date(); - localStorage.setItem('LAST_VISIT_POPUP', today.toISOString()); + setLastVisit(new Date(new Date().getTime() - (new Date().getTime() % (1000 * 60 * 60 * 24))).toDateString()); setShowPopUp(false); }; @@ -64,25 +52,28 @@ const PWAInfoPopUp = ({}: {}) => { <> - - - - - - - 24시간 동안 안보기 - 사용법 보기 - +

    + + + + + + +
    ) diff --git a/src/components/PopUp/PWAinfoPopUp/PWAinfoPopUp.style.ts b/src/components/PopUp/PWAinfoPopUp/PWAinfoPopUp.style.ts index 55c5e2ed..7c409b18 100644 --- a/src/components/PopUp/PWAinfoPopUp/PWAinfoPopUp.style.ts +++ b/src/components/PopUp/PWAinfoPopUp/PWAinfoPopUp.style.ts @@ -1,12 +1,19 @@ import styled from '@emotion/styled'; import { theme } from '@styles/themes'; +const Backdrop = styled('div')({ + position: 'fixed', + top: 0, + left: 0, + width: '100%', + height: '100%', + background: 'rgba(0, 0, 0, 0.5)', + zIndex: 999, +}); + const PWAInfoContainer = styled.div({ - display: 'flex', position: 'fixed', - bottom: '0', - left: '50%', - transform: 'translateX(-50%)', + bottom: '0px', borderRadius: '12px 12px 0 0', boxShadow: '0 4px 10px rgba(0, 0, 0, 0.2)', zIndex: 1000, @@ -16,78 +23,84 @@ const PWAInfoContainer = styled.div({ color: 'black', fontFamily: 'Pretendard', fontStyle: 'normal', + pointerEvents: 'auto', + padding: '32px 24px', + boxSizing: 'border-box', + gap: '24px', + display: 'flex', + + ['>svg']: { + position: 'absolute', + bottom: '100%', + right: '0px', + margin: '4px', + width: '36px', + height: 'auto', + aspectRatio: '1 / 1', + fill: theme.colors.sub_gray5, + }, }); -const DetailContainer = styled('div')({ +const PWAInfoContents = styled.div({ display: 'flex', - flexDirection: 'row', - alignContent: 'flex-start', - alignItems: 'flex-start', + gap: '16px', + alignItems: 'center', justifyContent: 'center', - gap: '15px', - padding: '30px', -}); -const TextArea = styled('div')({ - flexDirection: 'row', - gap: '15px', + ['>img']: { + maxWidth: '150px', + width: '100%', + minWidth: '0', + }, }); -const HeaderText = styled('h2')({}); +const PWAInfoTextContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '10px', + flexShrink: '0', + + ['>p']: { + margin: '0', + color: theme.colors.sub_black, + + ['&.title']: { + ...theme.font.heading24Bold, + }, -const NormalText = styled('div')({ - fontWeight: 500, + ['&.description']: { + ...theme.font.body16Medium, + }, + }, }); -const ButtonContainer = styled('div')({ +const PWAInfoButtonContainer = styled.div({ display: 'flex', - padding: '0 30px 30px 30px', justifyContent: 'center', gap: '20px', fontStyle: 'normal', -}); -const StyledButton = styled('button')({ - fontSize: '15px', - lineHeight: 1.5, - width: '160px', - borderRadius: '8px', - padding: '16px', - fontFamily: 'Pretendard', - fontWeight: 700, -}); - -const Close24HourButton = styled(StyledButton)({ - backgroundColor: theme.colors.primary0, - color: theme.colors.grayscale90, - border: `1px solid ${theme.colors.grayscale10}`, -}); - -const ConfirmButton = styled(StyledButton)({ - backgroundColor: theme.colors.primary50, - color: theme.colors.grayscale5, - border: 'none', -}); - -const Backdrop = styled('div')({ - position: 'fixed', - top: 0, - left: 0, - width: '100%', - height: '100%', - background: 'rgba(0, 0, 0, 0.5)', - zIndex: 999, -}); + ['>button']: { + width: '160px', + borderRadius: '8px', + padding: '12px 8px', + fontFamily: 'Pretendard', + ...theme.font.body14Semibold, + wordBreak: 'keep-all', + outline: 'none', + cursor: 'pointer', + border: `1px solid transparent`, -const CloseButton = styled('button')({ - position: 'absolute', - top: '-40px', - right: '-20px', - background: 'none', - border: 'none', - fontSize: '30px', - cursor: 'pointer', - color: theme.colors.primary0, + ['&.white']: { + background: theme.colors.sub_white, + color: theme.colors.sub_gray8, + borderColor: theme.colors.sub_gray2, + }, + ['&.blue']: { + background: theme.colors.sub_blue6, + color: theme.colors.sub_white, + }, + }, }); -export { PWAInfoContainer, DetailContainer, HeaderText, NormalText, TextArea, ButtonContainer, Close24HourButton, ConfirmButton, Backdrop, CloseButton }; +export { Backdrop, PWAInfoContainer, PWAInfoContents, PWAInfoTextContainer, PWAInfoButtonContainer }; diff --git a/src/components/PopUp/ZipyoPopUp/ZipyoPopUp.style.ts b/src/components/PopUp/ZipyoPopUp/ZipyoPopUp.style.ts deleted file mode 100644 index b5ba5cc8..00000000 --- a/src/components/PopUp/ZipyoPopUp/ZipyoPopUp.style.ts +++ /dev/null @@ -1,172 +0,0 @@ -import styled from '@emotion/styled'; -import { media, theme, themeColor } from '@styles/themes'; - -const PopUpContainer = styled('div')({ - display: 'flex', - flexDirection: 'column', - position: 'fixed', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: '568px', - height: 'auto', - background: theme.colors.grayscale30, - color: theme.colors.primary100, - borderRadius: '12px', - boxShadow: '0 4px 10px rgba(0, 0, 0, 0.2)', - zIndex: 1000, - fontFamily: 'Pretendard', - - [media[0]]: { - width: '90%', - ['svg']: { - width: '40%', - }, - }, -}); - -const PopUpImage = styled('ul')({ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - gap: '8px', - padding: 0, - listStyle: 'none', - - ['div']: { - display: 'flex', - flexDirection: 'column', - alignItems: 'left', - justifyContent: 'space-between', - gap: '8px', - flex: 1, - height: '160px', - - ['p']: { - textAlign: 'center', - fontWeight: '700', - fontSize: '16px', - margin: 0, - background: theme.colors.grayscale90, - color: theme.colors.primary0, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: '100%', - borderRadius: '8px', - width: '100%', - }, - - ['img']: { - height: '100%', - width: '100%', - borderRadius: '8px', - }, - }, - - [media[0]]: { - ['div']: { - height: '120px', - ['img']: { - height: '100%', - width: '100%', - borderRadius: '8px', - }, - }, - }, -}); - -const PopUpDetailContainer = styled('div')({ - display: 'flex', - flexDirection: 'column', - gap: '12px', - background: theme.colors.grayscale10, - borderRadius: '8px', - padding: '16px', - marginTop: '12px', - - [media[0]]: { - padding: '12px', // 모바일 내부 여백 축소 - marginTop: '8px', - }, -}); - -const PopUpDetail = styled('div')({ - display: 'flex', - alignItems: 'center', - gap: '12px', - - ['span']: { - fontSize: '14px', - color: theme.colors.grayscale100, - }, - - [media[0]]: { - ['span']: { - fontSize: '9px', - }, - }, -}); - -const PopUpDetailNumber = styled.div(({ color }: { color?: themeColor }) => ({ - width: '24px', - height: '24px', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - fontSize: '14px', - fontWeight: '700', - borderRadius: '4px', - background: color ? theme.colors[color] : theme.colors.primary40, - color: theme.colors.primary0, - - [media[0]]: { - width: '20px', // 모바일 크기 축소 - height: '20px', - fontSize: '12px', - }, -})); - -const ConfirmButton = styled('div')({ - textAlign: 'center', - fontWeight: '500', - fontSize: '16px', - cursor: 'pointer', - background: theme.colors.primary50, - color: theme.colors.primary0, - borderRadius: '0 0 12px 12px', - padding: '27px 0', - - [media[0]]: { - fontSize: '14px', // 모바일 글자 크기 축소 - padding: '16px 0', // 모바일 패딩 축소 - }, -}); - -const Backdrop = styled('div')({ - position: 'fixed', - top: 0, - left: 0, - width: '100%', - height: '100%', - background: 'rgba(0, 0, 0, 0.5)', - zIndex: 999, -}); - -const CloseButton = styled('button')({ - position: 'absolute', - top: '12px', - right: '12px', - background: 'none', - border: 'none', - fontSize: '18px', - cursor: 'pointer', - color: theme.colors.grayscale100, - - [media[0]]: { - top: '12px', - right: '0px', - }, -}); - -export { PopUpContainer, PopUpImage, PopUpDetailContainer, PopUpDetail, PopUpDetailNumber, ConfirmButton, Backdrop, CloseButton }; diff --git a/src/components/PopUp/ZipyoPopUp/ZipyoPopUp.tsx b/src/components/PopUp/ZipyoPopUp/ZipyoPopUp.tsx deleted file mode 100644 index 49c47ac5..00000000 --- a/src/components/PopUp/ZipyoPopUp/ZipyoPopUp.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import LogoSVG from '@assets/logo_blue.svg?react'; -import badPNG from '@assets/stockScore/bad.png'; -import CommonPopUp from '../CommonPopUp'; -import { PopUpContent, PopUpTitle } from '../CommonPopUp.style'; -import { PopUpDetail, PopUpDetailContainer, PopUpDetailNumber, PopUpImage } from './ZipyoPopUp.style'; - -const ZipyoPopUp = ({ onClose }: { onClose: () => void }) => ( - - - - 점수란 - - - 인간지표만의 알고리즘을 사용하여 주식 관련 커뮤니티의 댓글을 분석해 민심을 점수화했어요. 점수는 하루에 한 번씩 업데이트돼요. - -
    - 1 -

    "극대노"

    -
    -
    - 2 - 민심 이미지 -
    -
    - 3 -

    14점

    -
    -
    - - - 1 - 민심 점수를 한 단어로 설명하는 키워드에요 - - - 2 - 민심 점수에 해당하는 이미지에요. - - - 3 - - 인간지표만의 알고리즘으로 도출된 종목에 대한 민심 점수에요.
    - 점수가 높을수록 현재 개미들의 민심이 좋다는 것을 의미해요. -
    -
    -
    -
    -
    -); - -export default ZipyoPopUp; diff --git a/src/components/Search/GuageChart/GuageChart.Style.ts b/src/components/Search/GuageChart/GuageChart.Style.ts new file mode 100644 index 00000000..6db06ada --- /dev/null +++ b/src/components/Search/GuageChart/GuageChart.Style.ts @@ -0,0 +1,149 @@ +import styled from '@emotion/styled'; +import { theme } from '@styles/themes'; +import BalloonMaskPNG from '@assets/mask_balloon.png'; + +const ARC_COLORS = [ + theme.colors.sub_blue9, + theme.colors.sub_blue8, + theme.colors.sub_blue7, + theme.colors.sub_blue6, + theme.colors.sub_blue5, +]; + +const GuageChartContainer = styled.div({ + height: 'auto', + width: '100%', + aspectRatio: '7 / 4', + position: 'relative', + overflow: 'hidden', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + boxSizing: 'border-box', +}); + +const GuageChartContentsInner = styled.div({ + position: 'absolute', + bottom: '0', + transform: 'translateY(50%)', + width: '100%', + height: 'auto', + aspectRatio: '1 / 1', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}); + +const GuageChartItem = styled.div({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}); + +const GuageChartItemArc = styled.span( + ({ index, selected }: { index: number; selected: boolean }) => ({ + width: selected ? '80%' : '75%', + opacity: selected ? 1 : 0.5, + filter: selected ? `drop-shadow(0px 4px 8px rgba(0, 0, 0, 0.5))` : 'none', + + ['::after']: { + background: `conic-gradient(from ${-90 + index * 36}deg, ${selected ? theme.colors.sub_blue6 : ARC_COLORS[index]} 0deg, ${selected ? theme.colors.sub_blue6 : ARC_COLORS[index]} 36deg, transparent 36deg), + radial-gradient(circle at center, transparent 50%, transparent 50%)`, + }, + }), + { + position: 'absolute', + height: 'auto', + aspectRatio: '1 / 1', + + ['::after']: { + content: '""', + display: 'block', + width: '100%', + height: '100%', + borderRadius: '50%', + mask: `radial-gradient(closest-side, transparent calc(40%), #000 0)`, + }, + }, +); + +const GuageChartItemText = styled.span( + ({ index, selected }: { index: number; selected: boolean }) => ({ + left: `calc(50% + sin(-90deg + ${index} * 36deg + 18deg) * ${selected ? 0.56 : 0.525} * 50%)`, + top: `calc(50% - cos(-90deg + ${index} * 36deg + 18deg) * ${selected ? 0.56 : 0.525} * 50%)`, + ...theme.font[selected ? 'body18Semibold' : 'body16Semibold'], + opacity: selected ? 1 : 0.5, + }), + { + position: 'absolute', + transform: `translate(-50%, -50%)`, + margin: '0px', + color: theme.colors.sub_white, + }, +); + +const GuageChartItemBalloon = styled.div( + ({ index }: { index: number }) => ({ + left: `calc(50% + sin(-90deg + ${index} * 36deg + 18deg) * 0.65 * 50%)`, + top: `calc(50% - cos(-90deg + ${index} * 36deg + 18deg) * 0.65 * 50%)`, + transform: `translate(-50%, calc(${index === 2 ? -100 : index === 1 || index === 3 ? -105 : -115}%))`, + }), + { + zIndex: '4', + position: 'absolute', + + width: '30%', + height: 'auto', + aspectRatio: '5 / 4', + display: 'flex', + filter: 'drop-shadow(0px 4px 16px rgba(0, 0, 0, 0.25))', + + ['>img']: { + width: '100%', + height: '100%', + objectFit: 'cover', + maskImage: `url(${BalloonMaskPNG})`, + maskSize: '100% 100%', + }, + }, +); + +const GuageChartItemScorePlaceholder = styled.p( + ({ index }: { index: number }) => ({ + left: `calc(50% + sin(-90deg + ${index} * 36deg) * 0.8 * 50%)`, + top: `calc(50% - cos(-90deg + ${index} * 36deg) * 0.8 * 50%)`, + transform: `translate(${index < 2 ? -100 : index < 4 ? -50 : 0}%, -100%)`, + }), + { + position: 'absolute', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + padding: '0px 4px', + color: theme.colors.sub_white, + ...theme.font.body16Medium, + opacity: '0.2', + margin: '0', + }, +); + +const GuageChartItemScore = styled.p({ + position: 'absolute', + bottom: '0', + left: '50%', + transform: 'translateX(-50%)', + color: theme.colors.sub_blue6, + ...theme.font.title20Semibold, + marginBottom: '4px', +}); + +export { + GuageChartContainer, + GuageChartContentsInner, + GuageChartItem, + GuageChartItemArc, + GuageChartItemText, + GuageChartItemBalloon, + GuageChartItemScorePlaceholder, + GuageChartItemScore, +}; diff --git a/src/components/Search/GuageChart/GuageChart.tsx b/src/components/Search/GuageChart/GuageChart.tsx new file mode 100644 index 00000000..296859ea --- /dev/null +++ b/src/components/Search/GuageChart/GuageChart.tsx @@ -0,0 +1,53 @@ +import { scoreToImage, scoreToIndex } from '@utils/ScoreConvert'; +import { + GuageChartContainer, + GuageChartContentsInner, + GuageChartItem, + GuageChartItemArc, + GuageChartItemBalloon, + GuageChartItemScore, + GuageChartItemScorePlaceholder, + GuageChartItemText, +} from './GuageChart.Style'; + +const GuageChart = ({ score }: { score: number }) => { + const scoreText = ['대곰탕', '곰탕', '어?', '"호황"', '대호황!']; + const scoreIndex = scoreToIndex(score); + const scoreImage = scoreToImage(score); + const scoreRange = [0, 30, 40, 50, 70, 100]; + + return ( + + + {scoreRange.map((e, index) => { + return ( + + {e} + + ); + })} + + + {scoreText.map((e, index) => { + return ( + + + + {e} + + + {index === scoreIndex && ( + + score-image + + )} + + ); + })} + + {score}점 + + ); +}; + +export default GuageChart; diff --git a/src/components/Search/SearchTitle/SearchTitle.Style.ts b/src/components/Search/SearchTitle/SearchTitle.Style.ts index b10da112..f7e26085 100644 --- a/src/components/Search/SearchTitle/SearchTitle.Style.ts +++ b/src/components/Search/SearchTitle/SearchTitle.Style.ts @@ -1,6 +1,7 @@ import styled from '@emotion/styled'; import { motion } from 'framer-motion'; -import { media, theme, themeColor } from '@styles/themes'; +import { deltaScoreToColor } from '@utils/ScoreConvert'; +import { media, theme } from '@styles/themes'; export const SearchTitleContainer = styled.div({ display: 'flex', @@ -10,85 +11,20 @@ export const SearchTitleContainer = styled.div({ width: '100%', maxWidth: '1280px', margin: '0 auto', + padding: '0px 20px', color: theme.colors.grayscale30, - background: theme.colors.primary100, [media[0]]: { - gap: '24px', - padding: '0 20px', + gap: '12px', }, }); export const SearchTitleHeaderContainer = styled.div({ - display: 'flex', - alignItems: 'end', - justifyContent: 'space-between', -}); - -export const SearchTitleHeaderSymbol = styled.p({ - margin: '0', - padding: '8px 16px', - - fontSize: '15px', - - background: theme.colors.grayscale100, - borderRadius: '24px', - - [media[0]]: { - padding: '4px 12px', - - fontSize: '13px', - }, -}); - -export const SearchTitleHeaderButton = styled.div({ - display: 'flex', - gap: '12px', - alignItems: 'center', - padding: '12px 20px', - - fontWeight: '700', - fontSize: '17px', - lineHeight: '1', - - background: theme.colors.primary50, - borderRadius: '8px', - cursor: 'pointer', - - ['svg']: { - stroke: theme.colors.primary0, - - strokeWidth: '1.5', - }, - - [media[0]]: { - gap: '8px', - padding: '12px 16px', - - fontSize: '15px', - - ['svg']: { - width: '16px', - height: '16px', - }, - }, -}); - -export const SearchTitleBody = styled.div({ display: 'flex', flexDirection: 'column', - gap: '16px', - - [media[0]]: { - gap: '12px', - }, -}); - -export const SearchTitleBodyTitle = styled.div({ - display: 'flex', - alignItems: 'center', + // alignItems: 'center', fontWeight: '700', fontSize: '42px', @@ -98,9 +34,16 @@ export const SearchTitleBodyTitle = styled.div({ [media[0]]: { fontSize: '32px', }, + + ['>p.price']: { + ...theme.font.heading24Semibold, + color: theme.colors.sub_white, + margin: '0', + }, }); -export const SearchTitleBodyTitleText = styled.div({ +export const SearchTitleHeaderText = styled.div({ + ...theme.font.heading24Semibold, position: 'relative', overflow: 'hidden', @@ -110,7 +53,7 @@ export const SearchTitleBodyTitleText = styled.div({ textOverflow: 'ellipsis', }); -export const SearchTitleBodyTitleAnimatedText = styled(motion.div)({ +export const SearchTitleHeaderTextAnimated = styled(motion.div)({ willChange: 'transform', position: 'absolute', top: '0', @@ -118,77 +61,91 @@ export const SearchTitleBodyTitleAnimatedText = styled(motion.div)({ color: theme.colors.primary0, }); -export const SearchTitleBodyTitleSVG = styled.div({ - display: 'flex', - paddingLeft: '12px', - - ['svg']: { - width: '85px', - marginRight: 'auto', +// - textWrap: 'nowrap', - overflowWrap: 'anywhere', +export const SearchTitleDetailContainer = styled.div( + ({ delta }: { delta: number }) => ({ + ['>span.price-diff']: { + color: deltaScoreToColor(delta) ?? theme.colors.sub_gray7, + }, + }), + { + display: 'flex', + alignItems: 'center', + gap: '8px', - fill: theme.colors.primary50, - }, + ['>span']: { + ...theme.font.body14Medium, - [media[0]]: { - paddingLeft: '8 px', + ['&.market-code']: { + color: theme.colors.sub_gray4, + }, + }, - ['svg']: { - width: '56px', + ['>*']: { + display: 'flex', + alignItems: 'center', + ['&:not(:last-of-type)']: { + ['::after']: { + content: '""', + display: 'block', + width: '1px', + height: '12px', + background: theme.colors.sub_gray6, + marginLeft: '8px', + }, + }, }, }, -}); +); -export const SearchTitleBodySubtitle = styled.div({ +export const SearchTitleDetailSymbol = styled.span({ display: 'flex', - flexDirection: 'column', + alignItems: 'center', + gap: '4px', - fontSize: '15px', + ['>p']: { + ...theme.font.body14Medium, + color: theme.colors.sub_gray4, + margin: '0', + }, - [media[0]]: { - fontSize: '11px', + ['>img']: { + height: '20px', }, }); -export const SearchTitleFooterContainer = styled.div({ - display: 'flex', - gap: '12px', -}); +// -export const SearchTitleFooterItems = styled.div( +export const SearchTitleDescriptionContainer = styled.div( + ({ showMoreDesc }: { showMoreDesc: boolean }) => ({ + WebkitLineClamp: showMoreDesc ? '' : '2', + ['>button']: { + display: showMoreDesc ? 'none' : 'block', + }, + }), { - display: 'flex', - gap: '8px', - alignItems: 'center', - padding: '12px 18px', - - fontWeight: '700', - fontSize: '17px', - lineHeight: '1', - - background: theme.colors.grayscale100, - borderRadius: '8px', - - ['span']: { - fontWeight: '500', - fontSize: '15px', + overflow: 'hidden', + WebkitBoxOrient: 'vertical', + display: '-webkit-box', + + ['>p']: { + ...theme.font.body14Medium, + color: theme.colors.sub_gray4, + margin: '0', + whiteSpace: 'pre-line', }, - [media[0]]: { - padding: '8px 12px', - - fontSize: '15px', - - ['span']: { - fontSize: '13px', - }, + ['>button']: { + ...theme.font.body14Medium, + color: theme.colors.sub_gray4, + float: 'right', + marginTop: '21px', + marginRight: '8px', + padding: '0px', + background: 'none', + border: 'none', + shapeOutside: 'border-box', }, }, - ({ delta }: { delta?: themeColor }) => ({ - ['span']: { - color: theme.colors[delta ?? 'primary0'], - }, - }), ); diff --git a/src/components/Search/SearchTitle/SearchTitle.tsx b/src/components/Search/SearchTitle/SearchTitle.tsx index 92da0fa5..a7f8f890 100644 --- a/src/components/Search/SearchTitle/SearchTitle.tsx +++ b/src/components/Search/SearchTitle/SearchTitle.tsx @@ -1,45 +1,29 @@ import { AnimatePresence, Variants, useCycle } from 'framer-motion'; import { useEffect, useRef, useState } from 'react'; -import { useLocation } from 'react-router-dom'; -import { MARKET_CODES, ResultInfo } from '@ts/Constants'; -import { RESULT_TYPE } from '@ts/Types'; -import { deltaColor } from '@utils/Delta'; -import { StockDetailInfo } from '@controllers/api.Type'; -import { useStockSummaryQuery } from '@controllers/query'; -import RightSVG from '@assets/icons/right.svg?react'; -import ZipyoSVG from '@assets/zipyo.svg?react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { MARKET_CODES } from '@ts/Constants'; +import useAuthInfo from '@hooks/useAuthInfo'; +import { webPath } from '@router/index'; +import Button from '@components/Common/Button'; +import { useBuyExperimentMutation } from '@controllers/experiment/query'; +import { useStockSummaryQuery } from '@controllers/stocks/query'; +import { StockDetailInfo } from '@controllers/stocks/types'; +import KoreaPNG from '@assets/flags/korea.png'; import { - SearchTitleBody, - SearchTitleBodySubtitle, - SearchTitleBodyTitle, - SearchTitleBodyTitleAnimatedText, - SearchTitleBodyTitleSVG, - SearchTitleBodyTitleText, SearchTitleContainer, - SearchTitleFooterContainer, - SearchTitleFooterItems, - SearchTitleHeaderButton, + SearchTitleDescriptionContainer, + SearchTitleDetailContainer, + SearchTitleDetailSymbol, SearchTitleHeaderContainer, - SearchTitleHeaderSymbol, + SearchTitleHeaderText, + SearchTitleHeaderTextAnimated, } from './SearchTitle.Style'; const BASE_DELAY = 1500; -const priceDiff = (diff: number) => `${(diff < 0 ? '-' : '+') + Math.abs(diff).toLocaleString()}`; - -const SearchTitle = ({ - stockInfo, - resultMode, - onClick, -}: { - stockInfo: StockDetailInfo; - resultMode: RESULT_TYPE; - onClick: (e: any) => void; -}) => { +const SearchTitleName = ({ stockInfo: { symbolName, country, price } }: { stockInfo: StockDetailInfo }) => { const { state } = useLocation(); - const money = stockInfo.country === 'KOREA' ? '₩' : '$'; - const titleTextRef = useRef(null); const [animated, setAnimated] = useState(false); @@ -49,8 +33,25 @@ const SearchTitle = ({ instant: BASE_DELAY, }); const [animation, cycleAnimation] = useCycle(...Object.keys(animationDelay)); + const concurrency = country === 'KOREA' ? '₩' : '$'; + + useEffect(() => { + if (titleTextRef.current) { + const { offsetWidth, scrollWidth } = titleTextRef.current; + setAnimated(scrollWidth > offsetWidth); + setAnimationDelay({ + ...animationDelay, + animate: BASE_DELAY * (scrollWidth / offsetWidth - 1) * 2, + }); + } + }, [state]); - const [summary] = useStockSummaryQuery(stockInfo.symbol, stockInfo.country); + useEffect(() => { + const interval = setInterval(() => { + if (animated) cycleAnimation(); + }, animationDelay[animation]); + return () => clearInterval(interval); + }, [animation, animated]); const variants: Variants = { initial: { @@ -74,61 +75,89 @@ const SearchTitle = ({ }, }; - useEffect(() => { - if (titleTextRef.current) { - const { offsetWidth, scrollWidth } = titleTextRef.current; - setAnimated(scrollWidth > offsetWidth); - setAnimationDelay({ - ...animationDelay, - animate: BASE_DELAY * (scrollWidth / offsetWidth - 1) * 2, - }); + return ( + + + {symbolName} + + + {symbolName} + + + +

    + {concurrency} + {price.toLocaleString()} +

    +
    + ); +}; + +const SearchTitleDetail = ({ + stockInfo: { exchangeNum, symbol, priceDiff, priceDiffPerCent }, +}: { + stockInfo: StockDetailInfo; +}) => { + const marketCode = MARKET_CODES[exchangeNum]; + + const diffSign = priceDiff > 0 ? '+' : priceDiff < 0 ? '-' : ''; + const diffPercentText = Math.abs(priceDiffPerCent).toFixed(2); + const diffValueText = diffSign + Math.abs(priceDiff).toLocaleString(); + + return ( + + {marketCode} + +

    {symbol}

    + arrow +
    + + {diffValueText} + {`(${diffPercentText}%)`} + +
    + ); +}; + +const SearchTitle = ({ stockInfo }: { stockInfo: StockDetailInfo }) => { + const { data: summary = [], isLoading } = useStockSummaryQuery(stockInfo.symbol, stockInfo.country); + const navigate = useNavigate(); + const { isLogin } = useAuthInfo(); + + const { mutate: buyExperiment } = useBuyExperimentMutation(); + + const handleClickBuy = () => { + if (!isLogin) { + navigate(webPath.labStep(), { state: { step: 0 } }); + return; } - }, [state]); + buyExperiment({ stockId: stockInfo.stockId, country: stockInfo.country }); + navigate(webPath.labStep(), { state: { step: 3 } }); + }; - useEffect(() => { - const interval = setInterval(() => { - if (animated) cycleAnimation(); - }, animationDelay[animation]); - return () => clearInterval(interval); - }, [animation, animated]); + const [showMoreDesc, setShowMoreDesc] = useState(false); + + const handleClickMore = () => { + setShowMoreDesc(true); + }; return ( stockInfo && ( - - {stockInfo.symbol} - - {ResultInfo[ResultInfo[resultMode].opposite].text} 보기 - - - - - - - {stockInfo.symbolName} - - - {stockInfo.symbolName} - - - - - - - - - {summary.map((e, i) => ( - {e} - ))} - - - - {MARKET_CODES[stockInfo.exchangeNum]} - - {money} {stockInfo.price.toLocaleString()} - {`${priceDiff(stockInfo.priceDiff)}(${stockInfo.priceDiffPerCent}%)`} - - + + + {!isLoading && ( + + +

    + {summary.reduce((acc, e, i) => { + return acc + (i ? '\n' : '') + e; + }, '')} +

    +
    + )} + +
    ) ); diff --git a/src/components/Search/StockChart/StockChart.Style.ts b/src/components/Search/StockChart/StockChart.Style.ts index 8893abaa..b35aeda5 100644 --- a/src/components/Search/StockChart/StockChart.Style.ts +++ b/src/components/Search/StockChart/StockChart.Style.ts @@ -5,6 +5,7 @@ export const StockChartContainer = styled.div({ display: 'flex', flexDirection: 'column', gap: '18px', + height: '100%', [media[0]]: { gap: '12px', @@ -187,6 +188,7 @@ export const StockChartCanvasRefContainer = styled.canvas({ export const StockChartViewContainer = styled.div({ display: 'flex', + flexGrow: '1', fontSize: '15px', @@ -216,13 +218,31 @@ export const StockChartItemContent = styled.div( userSelect: 'none', }, - ({ type }: { type?: 'price' | 'score' }) => ({ - height: !type ? 'auto' : type == 'price' ? '500px' : '200px', + ({ type, chartHeight }: { type?: 'price' | 'score'; chartHeight?: { price: string; score: string } }) => ({ + height: !type + ? 'auto' + : chartHeight + ? type == 'price' + ? chartHeight.price + : chartHeight.score + : type == 'price' + ? '500px' + : '200px', borderBottom: type ? `2px solid ${theme.colors.grayscale90}` : '', + flexShrink: '0', + boxSizing: 'border-box', [media[0]]: { - height: !type ? 'auto' : type == 'price' ? '300px' : '100px', + height: !type + ? 'auto' + : chartHeight + ? type == 'price' + ? chartHeight.price + : chartHeight.score + : type == 'price' + ? '300px' + : '100px', }, }), ); diff --git a/src/components/Search/StockChart/StockChart.tsx b/src/components/Search/StockChart/StockChart.tsx index 2009dad6..3d0a2c7c 100644 --- a/src/components/Search/StockChart/StockChart.tsx +++ b/src/components/Search/StockChart/StockChart.tsx @@ -6,12 +6,13 @@ import { MAX_MIN, PERIOD_CODE_TEXT, } from '@ts/Constants'; -import { PERIOD_CODE, STOCK_COUNTRY } from '@ts/Types'; +import { StockCountryKey } from '@ts/StockCountry'; +import { PERIOD_CODE } from '@ts/Types'; import { drawLine, drawRect, setLineWidth } from '@utils/Canvas'; import { formatDateISO, getDateLabel } from '@utils/Date'; import { deltaColor } from '@utils/Delta'; import { useIsMobile } from '@hooks/useIsMobile'; -import { useStockChartQuery } from '@controllers/query'; +import { useStockChartQuery } from '@controllers/stocks/query'; import { theme, themeColor } from '@styles/themes'; import DownSVG from '@assets/icons/down.svg?react'; import UpSVG from '@assets/icons/up.svg?react'; @@ -177,15 +178,19 @@ const StockChartView = ({ updateChart, period, country, + chartHeight, + chartInteractive = true, }: { chartData: any[]; updateChart: any; period: PERIOD_CODE; - country: STOCK_COUNTRY; + country: StockCountryKey; + chartHeight?: { price: string; score: string }; + chartInteractive?: boolean; }) => { const isMobile = useIsMobile(); - const initialBarSize = isMobile ? 4 : 8; + const initialBarSize = isMobile ? 2 : 8; const GRID_GAP = { X: !isMobile ? 120 : 60, @@ -203,7 +208,7 @@ const StockChartView = ({ const [canvasPos, setCanvasPos, canvasPosRef] = useStateRef({ curr: { - x: 900, + x: 0, }, prev: { x: 0, @@ -264,7 +269,7 @@ const StockChartView = ({ setCanvasPos({ curr: { - x: width - GRID_GAP.X, + x: (3 * width) / 4, }, prev: { x: 0, @@ -364,7 +369,7 @@ const StockChartView = ({ const scoreChartList = getChartScoreItems(chartItems, scaledScore, scaledVolume); - drawPriceChart(dateGrid, chartPriceItems, chartSMAItems, recentPriceItem); + drawPriceChart(dateGrid, priceGrid, chartPriceItems, chartSMAItems, recentPriceItem); drawScoreChart(dateGrid, scoreChartList); }, [chartData, priceCanvasSize, canvasPos, barSize, priceScale, scoreScale]); @@ -438,6 +443,8 @@ const StockChartView = ({ const scoreLabel = scoreLabelRef.current; if (!scoreLabel) return; + if (!chartInteractive) return; + if (!isMobile) { charContainer.addEventListener('mousedown', handleCanvasPointerDown); window.addEventListener('mousemove', handleCanvasPointerMove); @@ -692,6 +699,7 @@ const StockChartView = ({ const drawPriceChart = ( dateGrid: any[], + priceGrid: any[], chartPriceItems: any[], chartSMAItems: Record< string, @@ -831,8 +839,8 @@ const StockChartView = ({ const width = chartContainerRef.current?.offsetWidth ?? 0; const itemWidth = scaledBarSize * barGap; - const MinX = itemWidth * (3 / 2); - const MaxX = width + itemWidth * (stateRef.current.chartLength - 5 / 2); + const MinX = (3 * width) / 4; + const MaxX = (3 * width) / 4 + itemWidth * stateRef.current.chartLength; const canvasX = x + (canvasPosRef.current.curr.x - x) * (scaledBarSize / barSize); const currX = canvasX < MinX ? MinX : canvasX > MaxX ? MaxX : canvasX; @@ -902,8 +910,8 @@ const StockChartView = ({ const width = chartContainerRef.current?.offsetWidth ?? 0; const itemWidth = barSize * barGap; - const MinX = itemWidth * (3 / 2); - const MaxX = width + itemWidth * (stateRef.current.chartLength - 5 / 2); + const MinX = (3 * width) / 4; + const MaxX = (3 * width) / 4 + itemWidth * stateRef.current.chartLength; const canvasX = canvasPosRef.current.curr.x + deltaX; const currX = canvasX < MinX ? MinX : canvasX > MaxX ? MaxX : canvasX; @@ -971,7 +979,7 @@ const StockChartView = ({ - + {extremePrice && Object.entries(extremePrice).map(([key, value]: [string, any]) => ( @@ -1007,7 +1015,7 @@ const StockChartView = ({ - + @@ -1032,44 +1040,49 @@ const StockChartView = ({ - - {priceGrid[priceGrid.length - 1]?.priceStr} - {priceGrid.map((e: any) => ( - - {e.valueStr} - - ))} - {lastPriceItem && ( - - {lastPriceItem.priceStr} - - )} - {recentPriceItem && ( - - {recentPriceItem.priceStr} - - )} - {mousePosInfo?.priceStr && ( - - {mousePosInfo.priceStr} - - )} - - - 100 - {scoreGrid.map( - (e: any) => - e.valueStr !== '' && ( - - {e.valueStr} - - ), - )} - {mousePosInfo?.scoreStr && ( - - {mousePosInfo.scoreStr} - - )} + + + {priceGrid[priceGrid.length - 1]?.priceStr} + {priceGrid.map((e: any) => ( + + {e.valueStr} + + ))} + {lastPriceItem && ( + + {lastPriceItem.priceStr} + + )} + {recentPriceItem && ( + + {recentPriceItem.priceStr} + + )} + {mousePosInfo?.priceStr && ( + + {mousePosInfo.priceStr} + + )} + + + 100 + {scoreGrid.map( + (e: any) => + e.valueStr !== '' && ( + + {e.valueStr} + + ), + )} + {mousePosInfo?.scoreStr && ( + + {mousePosInfo.scoreStr} + + )} + + + +
    @@ -1140,15 +1153,26 @@ const StockChart = ({ stockId, symbolName, country, + chartHeight, + chartInteractive, }: { stockId: number; symbolName: string; - country: STOCK_COUNTRY; + country: StockCountryKey; + chartHeight?: { price: string; score: string }; + chartInteractive?: boolean; }) => { const [selectedPeriod, setSelectedPeriod] = useState('D'); const [chartData, updateChartData] = useStockChartQuery(stockId, selectedPeriod); + const handlePeriodClick = (period: PERIOD_CODE) => (e: React.MouseEvent) => { + console.log(period); + e.preventDefault(); + e.stopPropagation(); + setSelectedPeriod(period); + }; + return ( @@ -1158,14 +1182,21 @@ const StockChart = ({ setSelectedPeriod(key)} + onPointerDown={handlePeriodClick(key)} > {value} ))} - + ); }; diff --git a/src/components/Search/StockWordCloud/StockWordCloud.tsx b/src/components/Search/StockWordCloud/StockWordCloud.tsx index 594958b3..60e5582e 100644 --- a/src/components/Search/StockWordCloud/StockWordCloud.tsx +++ b/src/components/Search/StockWordCloud/StockWordCloud.tsx @@ -1,10 +1,9 @@ import { useEffect, useRef, useState } from 'react'; -import { useLocation } from 'react-router-dom'; import { WordCloudItem } from '@ts/Interfaces'; -import { STOCK_COUNTRY } from '@ts/Types'; +import { StockCountryKey } from '@ts/StockCountry'; import { useIsMobile } from '@hooks/useIsMobile'; import LoadingComponent from '@components/Common/LoadingComponent'; -import { useWordCloudQuery } from '@controllers/query'; +import { useWordCloudQuery } from '@controllers/stocks/query'; import { StockWordCloudContainer, Word, WordCloudTestText, WordContainer } from './StockWordCloud.Style'; const StockWordCloudContents = ({ @@ -32,8 +31,7 @@ const StockWordCloudContents = ({ ); }; -const StockWordCloud = ({ symbol, country }: { symbol: string; country: STOCK_COUNTRY }) => { - const { state } = useLocation(); +const StockWordCloud = ({ symbol, country }: { symbol: string; country: StockCountryKey }) => { const isMobile = useIsMobile(); const containerRef = useRef(null); @@ -52,9 +50,8 @@ const StockWordCloud = ({ symbol, country }: { symbol: string; country: STOCK_CO const testTextRef = useRef(null); useEffect(() => { - if (symbol == state?.symbol) return; setCurrentIndex(-1); - }, [state]); + }, [symbol, country]); useEffect(() => { if (!wordCloud || currentIndex > wordCloud.length) return; diff --git a/src/components/SearchBar/AutoComplete/AutoComplete.Style.ts b/src/components/SearchBar/AutoComplete/AutoComplete.Style.ts new file mode 100644 index 00000000..1f4387db --- /dev/null +++ b/src/components/SearchBar/AutoComplete/AutoComplete.Style.ts @@ -0,0 +1,27 @@ +import styled from '@emotion/styled'; +import { theme } from '@styles/themes'; + +const AutoCompleteEmptyContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '6px', + alignItems: 'center', + padding: '88px 0px', + + ['>p']: { + margin: '0', + whiteSpace: 'nowrap', + + ['&.empty_title']: { + ...theme.font.body18Medium, + color: theme.colors.sub_gray7, + }, + + ['&.empty_subtitle']: { + ...theme.font.body14Medium, + color: theme.colors.sub_gray6, + }, + }, +}); + +export { AutoCompleteEmptyContainer }; diff --git a/src/components/SearchBar/AutoComplete/Keywords/Keywords.tsx b/src/components/SearchBar/AutoComplete/Keywords/Keywords.tsx new file mode 100644 index 00000000..3270ccf3 --- /dev/null +++ b/src/components/SearchBar/AutoComplete/Keywords/Keywords.tsx @@ -0,0 +1,60 @@ +import { useState } from 'react'; +import { useEffect } from 'react'; +import { SmallStockCard } from '@components/CardList/StockCard/StockCard'; +import { + SearchBarItemContainer, + SearchBarItemContents, + SearchBarItemTitle, +} from '@components/SearchBar/SearchBar.Style'; +import { fetchSearchKeyword } from '@controllers/stocks/api'; +import { useAutoComplete } from '@controllers/stocks/query'; +import { AutoCompleteEmptyContainer } from '../AutoComplete.Style'; + +const AutoCompleteKeywords = ({ searchValue }: { searchValue: string }) => { + const [searchedKeywords, setSearchedKeywords] = useAutoComplete(fetchSearchKeyword, 'keyword'); + const [matchedKeyword, setMatchedKeyword] = useState(''); + + useEffect(() => { + setSearchedKeywords(searchValue); + }, [searchValue]); + + useEffect(() => { + if (!searchedKeywords?.length) return; + setMatchedKeyword(searchValue); + }, [searchedKeywords]); + + return ( + + + 검색결과 + {matchedKeyword && ( +

    + '{matchedKeyword}'이(가) 가장 많이 언급된 종목순으로 노출됩니다 +

    + )} +
    + + {searchedKeywords?.length ? ( + searchedKeywords?.map(({ stockId, symbolName, score, diff, keywordNames }, index) => { + const stock = { + stockId: stockId, + symbolName: symbolName, + score: score, + diff: diff, + keywords: keywordNames.includes(matchedKeyword) ? keywordNames : [keywordNames[0], matchedKeyword], + }; + + return ; + }) + ) : ( + +

    '{searchValue}' 검색어에 해당하는 결과가 없어요 😭

    +

    다른 종목을 다시 검색해보세요

    +
    + )} +
    +
    + ); +}; + +export default AutoCompleteKeywords; diff --git a/src/components/SearchBar/AutoComplete/Stocks/Stocks.Style.ts b/src/components/SearchBar/AutoComplete/Stocks/Stocks.Style.ts new file mode 100644 index 00000000..a51d9319 --- /dev/null +++ b/src/components/SearchBar/AutoComplete/Stocks/Stocks.Style.ts @@ -0,0 +1,46 @@ +import styled from '@emotion/styled'; +import { theme } from '@styles/themes'; + +const AutoCompleteStocksItem = styled.div({ + display: 'flex', + alignItems: 'center', + gap: '12px', + overflow: 'hidden', + width: '100%', + + ['>p']: { + margin: '0', + + ['&.country']: { + ...theme.font.body14Medium, + color: theme.colors.sub_gray6, + flexShrink: '0', + }, + + ['&.name']: { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + ...theme.font.body16Semibold, + color: theme.colors.sub_gray1, + + ['>b']: { + ...theme.font.body16Semibold, + color: theme.colors.sub_blue5, + }, + + ['>span']: { + padding: '0 4px', + ...theme.font.detail12Semibold, + color: theme.colors.sub_gray6, + + ['>b']: { + ...theme.font.detail12Semibold, + color: theme.colors.sub_blue5, + }, + }, + }, + }, +}); + +export { AutoCompleteStocksItem }; diff --git a/src/components/SearchBar/AutoComplete/Stocks/Stocks.tsx b/src/components/SearchBar/AutoComplete/Stocks/Stocks.tsx new file mode 100644 index 00000000..9f75d1df --- /dev/null +++ b/src/components/SearchBar/AutoComplete/Stocks/Stocks.tsx @@ -0,0 +1,64 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { StockCountryKey } from '@ts/StockCountry'; +import { STOCK_COUNTRY_MAP } from '@ts/StockCountry'; +import extractMatchedSegments from '@utils/extractMatchedSegments'; +import useRecentStocks from '@hooks/useRecentStocks'; +import { webPath } from '@router/index'; +import { + SearchBarItemContainer, + SearchBarItemContents, + SearchBarItemTitle, +} from '@components/SearchBar/SearchBar.Style'; +import { fetchAutoComplete } from '@controllers/stocks/api'; +import { useAutoComplete } from '@controllers/stocks/query'; +import { AutoCompleteEmptyContainer } from '../AutoComplete.Style'; +import { AutoCompleteStocksItem } from './Stocks.Style'; + +const AutoCompleteStocks = ({ searchValue }: { searchValue: string }) => { + const navigate = useNavigate(); + const [searchedStocks, setSearchedStocks] = useAutoComplete(fetchAutoComplete, 'symbolName'); + const { addRecentStock } = useRecentStocks(); + + useEffect(() => { + setSearchedStocks(searchValue); + }, [searchValue]); + + const handleStockClick = (symbolName: string, country: StockCountryKey) => () => { + addRecentStock(symbolName, country); + + navigate(webPath.search(), { state: { symbolName: symbolName, country: country }, replace: true }); + }; + + return ( + + 검색결과 + + {searchedStocks?.length ? ( + searchedStocks.map(({ stockId, symbolName, country, symbol }) => ( + +

    {STOCK_COUNTRY_MAP[country].text}종목

    +

    + {extractMatchedSegments(symbolName, searchValue).map(({ matched, text }, index) => + matched ? {text} : text, + )} + + {extractMatchedSegments(symbol, searchValue).map(({ matched, text }, index) => + matched ? {text} : text, + )} + +

    +
    + )) + ) : ( + +

    '{searchValue}' 검색어에 해당하는 결과가 없어요 😭

    +

    다른 종목을 다시 검색해보세요

    +
    + )} +
    +
    + ); +}; + +export default AutoCompleteStocks; diff --git a/src/components/SearchBar/PopularKeywords/PopularKeywords.tsx b/src/components/SearchBar/PopularKeywords/PopularKeywords.tsx new file mode 100644 index 00000000..5f4dcfc5 --- /dev/null +++ b/src/components/SearchBar/PopularKeywords/PopularKeywords.tsx @@ -0,0 +1,62 @@ +import styled from '@emotion/styled'; +import { useKeywordRankingsQuery } from '@controllers/stocks/query'; +import { theme } from '@styles/themes'; + +const Wrapper = styled.div({ + flexGrow: 1, + display: 'flex', + flexDirection: 'column', + gap: '20px', + + ['>p']: { + ...theme.font.body18Semibold, + color: theme.colors.sub_white, + margin: '0 20px', + }, +}); + +const PopularKeywordsContainer = styled.div({ + display: 'flex', + padding: '0 20px', + overflow: 'auto', + gap: '8px', + + msOverflowStyle: 'none', + ['::-webkit-scrollbar']: { + display: 'none', + }, + + ['>span']: { + whiteSpace: 'nowrap', + overflow: 'hidden', + flexShrink: '0', + padding: '8px 16px', + borderRadius: '999px', + ...theme.font.body16Medium, + color: theme.colors.sub_gray1, + border: `1px solid ${theme.colors.sub_gray9}`, + }, +}); + +const PopularKeywords = ({ setSearchValue }: { setSearchValue: (value: string) => void }) => { + const { data: keywordRankings } = useKeywordRankingsQuery(); + + const handlePopularKeywordClick = (keyword: string) => () => { + setSearchValue(keyword); + }; + + return ( + +

    가장 많이 언급되는 키워드!

    + + {keywordRankings?.map((e) => ( + + {e} + + ))} + +
    + ); +}; + +export default PopularKeywords; diff --git a/src/components/SearchBar/PopularStocks/PopularStocks.Style.ts b/src/components/SearchBar/PopularStocks/PopularStocks.Style.ts new file mode 100644 index 00000000..4418ce5c --- /dev/null +++ b/src/components/SearchBar/PopularStocks/PopularStocks.Style.ts @@ -0,0 +1,51 @@ +import styled from '@emotion/styled'; +import { theme } from '@styles/themes'; + +const PopularStocksItem = styled.div({ + display: 'flex', + alignItems: 'center', + gap: '16px', + + ['>p']: { + margin: '0', + ...theme.font.body16Semibold, + color: theme.colors.sub_blue6, + }, +}); + +const PopularStocksItemContents = styled.div({ + display: 'flex', + alignItems: 'center', + gap: '10px', + overflow: 'hidden', + flexGrow: 1, + + ['>img']: { + width: '32px', + height: '32px', + aspectRatio: '1 / 1', + borderRadius: '50%', + flexShrink: '0', + background: theme.colors.sub_gray11, + }, + + ['>p']: { + ...theme.font.body16Semibold, + color: theme.colors.sub_gray1, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + flexGrow: 1, + margin: '0', + }, + + ['>svg']: { + width: '24px', + height: 'auto', + aspectRatio: '1 / 1', + fill: theme.colors.sub_gray7, + flexShrink: '0', + }, +}); + +export { PopularStocksItem, PopularStocksItemContents }; diff --git a/src/components/SearchBar/PopularStocks/PopularStocks.tsx b/src/components/SearchBar/PopularStocks/PopularStocks.tsx new file mode 100644 index 00000000..fd4ab959 --- /dev/null +++ b/src/components/SearchBar/PopularStocks/PopularStocks.tsx @@ -0,0 +1,47 @@ +import { useNavigate } from 'react-router-dom'; +import { StockCountryKey } from '@ts/StockCountry'; +import useRecentStocks from '@hooks/useRecentStocks'; +import { webPath } from '@router/index'; +import StockImage from '@components/Common/StockImage'; +import { usePopularStockFetchQuery } from '@controllers/stocks/query'; +import ChevronLeftSVG from '@assets/icons/chevronLeft.svg?react'; +import { SearchBarItemContainer, SearchBarItemContents, SearchBarItemTitle } from '../SearchBar.Style'; +import { PopularStocksItem, PopularStocksItemContents } from './PopularStocks.Style'; + +const PopularStocks = () => { + const navigate = useNavigate(); + + const { addRecentStock } = useRecentStocks(); + + const [popularStocks] = usePopularStockFetchQuery(); + console.log(popularStocks); + + const handlePopularStockClick = (symbolName: string, country: StockCountryKey) => () => { + addRecentStock(symbolName, country); + + navigate(webPath.search(), { state: { symbolName: symbolName, country: country }, replace: true }); + }; + + return ( + + 인간지표 인기검색어 + + {popularStocks.map(({ stockId, symbolName, country }, index) => ( + +

    {index + 1}

    + + +

    {symbolName}

    + +
    +
    + ))} +
    +
    + ); +}; + +export default PopularStocks; diff --git a/src/components/SearchBar/RecentStocks/RecentStocks.Style.ts b/src/components/SearchBar/RecentStocks/RecentStocks.Style.ts new file mode 100644 index 00000000..04de8cdb --- /dev/null +++ b/src/components/SearchBar/RecentStocks/RecentStocks.Style.ts @@ -0,0 +1,53 @@ +import styled from '@emotion/styled'; +import { theme } from '@styles/themes'; + +const RecentStocksItem = styled.div({ + display: 'flex', + alignItems: 'center', + gap: '12px', + + ['>svg']: { + width: '24px', + height: 'auto', + aspectRatio: '1 / 1', + fill: theme.colors.sub_gray9, + flexShrink: '0', + }, +}); + +const RecentStocksItemContents = styled.div({ + display: 'flex', + flexGrow: 1, + alignItems: 'center', + gap: '8px', + overflow: 'hidden', + + ['>p']: { + margin: '0', + + ['&.country']: { + ...theme.font.body14Medium, + color: theme.colors.sub_gray6, + flexShrink: '0', + }, + + ['&.symbolName']: { + ...theme.font.body16Semibold, + color: theme.colors.sub_gray1, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + flexGrow: 1, + }, + }, + + ['>svg']: { + width: '24px', + height: 'auto', + aspectRatio: '1 / 1', + fill: theme.colors.sub_gray9, + flexShrink: '0', + }, +}); + +export { RecentStocksItem, RecentStocksItemContents }; diff --git a/src/components/SearchBar/RecentStocks/RecentStocks.tsx b/src/components/SearchBar/RecentStocks/RecentStocks.tsx new file mode 100644 index 00000000..ddc82196 --- /dev/null +++ b/src/components/SearchBar/RecentStocks/RecentStocks.tsx @@ -0,0 +1,46 @@ +import { useNavigate } from 'react-router-dom'; +import { STOCK_COUNTRY_MAP, StockCountryKey } from '@ts/StockCountry'; +import useRecentStocks from '@hooks/useRecentStocks'; +import { webPath } from '@router/index'; +import ClockSVG from '@assets/icons/clock.svg?react'; +import CrossSVG from '@assets/icons/cross.svg?react'; +import { SearchBarItemContainer, SearchBarItemContents, SearchBarItemTitle } from '../SearchBar.Style'; +import { RecentStocksItem, RecentStocksItemContents } from './RecentStocks.Style'; + +const RecentStocks = () => { + const navigate = useNavigate(); + + const { recentStocks, addRecentStock, removeRecentStock } = useRecentStocks(); + + const handleRecentStockDelete = (symbolName: string) => (e: React.MouseEvent) => { + e.stopPropagation(); + + removeRecentStock(symbolName); + }; + + const handleRecentStockClick = (symbolName: string, country: StockCountryKey) => () => { + addRecentStock(symbolName, country); + + navigate(webPath.search(), { state: { symbolName: symbolName, country: country }, replace: true }); + }; + + return ( + + 최근 검색어 + + {(recentStocks ?? []).map(({ symbolName, country }) => ( + + + +

    {STOCK_COUNTRY_MAP[country].text}종목

    +

    {symbolName}

    + +
    +
    + ))} +
    +
    + ); +}; + +export default RecentStocks; diff --git a/src/components/SearchBar/SearchBar.Style.ts b/src/components/SearchBar/SearchBar.Style.ts index 0ae8a107..57210d5f 100644 --- a/src/components/SearchBar/SearchBar.Style.ts +++ b/src/components/SearchBar/SearchBar.Style.ts @@ -1,428 +1,176 @@ -// SearchBar import styled from '@emotion/styled'; -import { media, theme } from '@styles/themes'; +import { theme } from '@styles/themes'; -export const SearchBarLayout = styled.div({ - padding: '0 60px 80px', - [media[0]]: { - position: 'relative', - padding: '0 20px 40px', - }, +const SearchBarLayout = styled.div({ + position: 'fixed', + top: 0, + left: '50%', + transform: 'translateX(-50%)', + width: '100%', + height: '100%', + background: 'rgba(0, 0, 0)', + zIndex: 1000, + display: 'flex', + flexDirection: 'column', + overflow: 'hidden auto ', + maxWidth: '1280px', }); -export const SearchBarContainer = styled.div( - { - display: 'flex', - padding: '12px', - gap: '12px', - fontWeight: '700', - color: theme.colors.primary5, - lineHeight: '1', - background: theme.colors.primary100, - outline: 'none', - position: 'relative', - - [media[0]]: { - position: 'static', - gap: '12px', - padding: '12px 12px', - }, - }, - (props: { active: boolean }) => ({ - borderRadius: !props.active ? '12px' : '12px 12px 0 0', - [media[0]]: { - borderRadius: '8px', - }, - }), -); +const SearchBarContainer = styled.div({ + padding: '20px 0', + flexGrow: 1, + display: 'flex', + flexDirection: 'column', + gap: '45px', +}); -export const SearchBarInput = styled.div({ +const SearchBarContents = styled.div({ + padding: '0 20px', display: 'flex', alignItems: 'center', - background: theme.colors.grayscale100, - padding: '18px', - borderRadius: '8px', - width: '100%', - fontSize: '24px', - ['input']: { - boxSizing: 'border-box', - width: '100%', - border: 'none', - background: theme.colors.transparent, - color: theme.colors.primary0, - outline: 'none', - fontFamily: 'Pretendard', - - '::placeholder': { - color: theme.colors.grayscale50, - }, - }, - ['svg']: { - height: '1em', - width: 'auto', - stroke: theme.colors.grayscale30, - cursor: 'pointer', - }, - [media[0]]: { - padding: '0 12px', - borderRadius: '4px', - fontSize: '18px', - ['input']: { - padding: '12px 0', - }, - }, + gap: '12px', }); -export const SearchBarSelectBox = styled.div( - { - position: 'relative', - display: 'flex', - width: '50%', - - ['label']: { - cursor: 'pointer', - fontSize: '17px', - borderRadius: '8px', - padding: '18px', - alignContent: 'center', - width: '100%', - background: theme.colors.grayscale100, - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - ['span']: { - transition: 'all 0.25s ease-in-out', - }, - ['svg']: { - fill: theme.colors.grayscale10, - height: '1em', - width: '1em', - }, - }, - [media[0]]: { - width: 'auto', - ['label']: { - ['span']: { - width: '75px', - overflow: 'hidden', - whiteSpace: 'nowrap', - }, - fontSize: '15px', - padding: '12px', - borderRadius: '4px', - }, - }, - }, - (props: { focus: boolean; minimize?: boolean }) => ({ - ...(props.focus && { - ['label']: { - background: theme.colors.grayscale90, - borderRadius: '8px 8px 0 0', - [media[0]]: { - borderRadius: '4px 4px 0 0', - }, - }, - ul: { - maxHeight: '150px', - }, - }), - ...(props.minimize && { - [media[0]]: { - ['label']: { - ['span']: { - width: '0px', - }, - }, - }, - }), - }), -); +const SearchBarSelectBox = styled.div({ + position: 'relative', + flexShrink: '0', -export const SearchBarSelectBoxItems = styled.ul( - { - cursor: 'pointer', - position: 'absolute', - zIndex: '20', - top: '100%', - listStyle: 'none', - padding: '0', - margin: '0', - width: '100%', + ['>label']: { display: 'flex', - maxHeight: '0', - flexDirection: 'column', alignItems: 'center', - borderRadius: '0 0 8px 8px', - overflow: 'hidden', - transition: 'max-height .25s ease-in-out', + justifyContent: 'space-between', + padding: '10px', + borderRadius: '8px', + cursor: 'pointer', + border: `1.5px solid transparent`, + background: theme.colors.sub_gray11, + + ...theme.font.body16Medium, - li: { - width: '100%', - background: theme.colors.grayscale100, - textAlign: 'center', - fontSize: '15px', - padding: '18px 0', - borderTop: `2px solid ${theme.colors.grayscale100}`, + ['>svg']: { + width: '24px', + height: 'auto', + aspectRatio: '1 / 1', }, - [media[0]]: { - borderRadius: '0 0 4px 4px', - li: { - fontSize: '13px', - padding: '12px 0', + [':focus']: { + borderColor: theme.colors.sub_gray7, + ['>svg']: { + transform: 'rotate(180deg);', + }, + + ['+ul']: { + display: 'block', + borderColor: theme.colors.sub_gray7, }, }, }, - (props: { select: number }) => ({ - [`li:nth-of-type(${props.select + 1})`]: { - background: theme.colors.grayscale90, - }, - }), -); - -// SearchBarResult -export const SearchBarResultLayoutContainer = styled.div( - { + ['>ul']: { + display: 'none', position: 'absolute', - zIndex: '10', - left: '0', - right: '0', top: '100%', - overflow: 'hidden', - height: 'auto', - - transition: 'max-height .25s ease-in-out', - background: theme.colors.primary100, - - [media[0]]: { - overflow: 'scroll', - height: '100vh', + left: '0', + listStyle: 'none', + padding: '0', + margin: '8px 0 0', + width: '100%', + zIndex: '1', + border: `1.5px solid ${theme.colors.sub_gray11}`, + borderRadius: '8px', + background: theme.colors.sub_gray11, + + ['>li']: { + padding: '12px 14px', + ...theme.font.body16Medium, + cursor: 'pointer', }, }, - (props: { height: number }) => ({ - maxHeight: props.height, - }), -); - -export const SearchBarResultLayout = styled.div({ - boxSizing: 'border-box', - width: '100%', - padding: '32px', - - [media[0]]: { - padding: '32px 20px', - }, }); -export const SearchBarResultContainer = styled.div({ +const SearchBarInput = styled.label({ display: 'flex', - gap: '24px', - width: '100%', - height: '100%', - - [media[0]]: { - flexDirection: 'column', - }, -}); - -export const SearchBarResultContent = styled.div( - { - display: 'flex', - flexDirection: 'column', - gap: '18px', - boxSizing: 'content-box', - height: '100%', - textTransform: 'uppercase', + background: theme.colors.sub_gray11, + borderRadius: '6px', + padding: '10px 16px', + flexGrow: 1, + minWidth: '0', + + ['>input']: { + flexGrow: '1', + background: 'none', + border: 'none', + outline: 'none', + ...theme.font.body16Medium, + color: theme.colors.sub_gray2, + minWidth: '0', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', - [media[0]]: { - gap: '12px', - width: '100%', + ['::placeholder']: { + color: theme.colors.sub_gray8, }, - }, - (props: { width: string }) => ({ - width: props.width, - }), -); -export const SearchBarResultSVG = styled.div({ - width: '100%', - height: '100%', - boxSizing: 'border-box', - padding: '72px', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - ['svg']: { - boxSizing: 'content-box', - minHeight: '260px', - width: 'auto', - height: '100%', + ['&:focus']: { + ['::placeholder']: { + color: 'transparent', + }, + }, }, - [media[0]]: { - padding: '32px', + ['>svg']: { + width: '24px', + height: 'auto', + aspectRatio: '1 / 1', + fill: theme.colors.sub_gray8, + flexShrink: '0', }, }); -// SearchBarResultTitle - -export const SearchBarResultTitle = styled.div({ - fontSize: '24px', +const SearchBarItemContainer = styled.div({ + padding: '0 20px', display: 'flex', - alignItems: 'center', - gap: '12px', - - [media[0]]: { - fontSize: '21px', - gap: '8px', - }, -}); - -export const SearchBarResultSubtitle = styled.span({ - color: theme.colors.grayscale30, - fontSize: '15px', - whiteSpace: 'nowrap', - fontWeight: '500', - - ['b']: { - fontWeight: '900', - }, - [media[0]]: { - fontSize: '13px', - }, + flexDirection: 'column', + gap: '20px', }); -// SearchBarResultItem - -export const SearchBarResultGridContainer = styled.div( - { - display: 'grid', - gridAutoFlow: 'column', - columnGap: '32px', - - ['> div']: { - padding: '8px 0', - borderBottom: `1px solid ${theme.colors.grayscale80}`, - overflow: 'hidden', +const SearchBarItemTitle = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: '8px', - ':last-child, :nth-of-type(5n)': { - border: 'none', - }, - }, + ...theme.font.body18Semibold, + color: theme.colors.sub_white, - [media[0]]: { - display: 'flex', - flexDirection: 'column', - ['> div:nth-of-type(5n):not(:last-child)']: { - borderBottom: `1px solid ${theme.colors.grayscale80}`, - }, - }, - }, - (props: { column: number }) => ({ - gridTemplateColumns: `repeat(${props.column}, 1fr)`, - gridTemplateRows: `repeat(5, ${props.column}fr)`, - }), -); + ['>p']: { + margin: '0', -export const SearchBarResultItemContainer = styled.div({ - display: 'flex', - alignItems: 'center', - gap: '12px', - padding: '9px 12px 9px 18px', - borderRadius: '0 32px 32px 0', - cursor: 'pointer', - fontSize: '18px', + ...theme.font.body14Medium, + color: theme.colors.sub_gray6, - [':hover']: { - background: theme.colors.grayscale100, - ['svg']: { - stroke: theme.colors.primary5, + ['>b']: { + flexShrink: '0', + ...theme.font.body14Semibold, + color: theme.colors.sub_gray3, + marginRight: '4px', }, }, - - [media[0]]: { - fontSize: '15px', - padding: '6px 9px 6px 12px', - // height: '18px', - gap: '8px', - }, }); -export const SearchBarResultItemSVG = styled.div({ - lineHeight: 1, - height: 'auto', +const SearchBarItemContents = styled.div({ display: 'flex', - alignItems: 'center', - ['> svg']: { - width: '.9em', - height: '.9em', - cursor: 'pointer', - borderRadius: '24px', - [':hover']: { - background: theme.colors.grayscale70, - }, - }, - [media[0]]: { - ['> svg']: { - stroke: theme.colors.primary5, - }, - }, + flexDirection: 'column', + gap: '20px', }); -export const SearchBarResultItemKeyword = styled.span( - { - padding: '8px 16px', - fontSize: '15px', - textWrap: 'nowrap', - borderRadius: '24px', - [media[0]]: { - padding: '6px 12px', - fontSize: '13px', - }, - }, - (props: { matched: boolean }) => ({ - background: props.matched ? theme.colors.primary50 : theme.colors.grayscale80, - }), -); - -export const SearchBarResultItemSubtitle = styled.span({ - color: theme.colors.grayscale40, - fontSize: '15px', - whiteSpace: 'nowrap', - [media[0]]: { - fontSize: '11px', - }, -}); - -export const SearchBarResultItemTitle = styled.span({ - color: theme.colors.primary0, - textOverflow: 'ellipsis', - overflow: 'hidden', - - fontSize: '19px', - marginRight: 'auto', - whiteSpace: 'nowrap', - ['span']: { - color: theme.colors.primary40, - }, - - ['> div']: { - display: 'flex', - gap: '8px', - fontSize: '15px', - paddingTop: '8px', - ['> div']: { - color: theme.colors.grayscale60, - }, - }, - - [media[0]]: { - fontSize: '15px', - ['> div']: { - paddingTop: '6px', - gap: '6px', - fontSize: '13px', - }, - }, -}); +export { + SearchBarLayout, + SearchBarContainer, + SearchBarContents, + SearchBarSelectBox, + SearchBarInput, + SearchBarItemContainer, + SearchBarItemTitle, + SearchBarItemContents, +}; diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx index 55316a63..3706270c 100644 --- a/src/components/SearchBar/SearchBar.tsx +++ b/src/components/SearchBar/SearchBar.tsx @@ -1,489 +1,95 @@ -import { useEffect, useRef, useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { MARKET_CODES, SEARCH_CATEGORY_TEXT, STOCK_COUNTRY_TEXT } from '@ts/Constants'; -import { SEARCH_CATEGORY } from '@ts/Types'; -import { STORAGE_RECENT_ITEMS, getItemLocalStorage, setItemLocalStorage } from '@utils/LocalStorage'; -import { useIsMobile } from '@hooks/useIsMobile'; -import { webPath } from '@router/index'; -import { fetchAutoComplete, fetchSearchKeyword } from '@controllers/api'; -import { AutoCompleteItem, SearchBarResultItems } from '@controllers/api.Type'; -import { PopularKeywordQuery, useAutoComplete, usePopularStockFetchQuery } from '@controllers/query'; -import CancelSVG from '@assets/icons/cancel.svg?react'; -import DownSVG from '@assets/icons/down.svg?react'; +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { SEARCH_CATEGORIES, SEARCH_CATEGORY_MAP, SearchCategoryKey } from '@ts/SearchCategory'; +import Header from '@layout/Header/Header'; +import ChevronDownSVG from '@assets/icons/chevronDown.svg?react'; +import CrossSVG from '@assets/icons/cross.svg?react'; import SearchSVG from '@assets/icons/search.svg?react'; -import UpSVG from '@assets/icons/up.svg?react'; -import NoResultSVG from '@assets/noResult.svg?react'; +import AutoCompleteKeywords from './AutoComplete/Keywords/Keywords'; +import AutoCompleteStocks from './AutoComplete/Stocks/Stocks'; +import PopularKeywords from './PopularKeywords/PopularKeywords'; +import PopularStocks from './PopularStocks/PopularStocks'; +import RecentStocks from './RecentStocks/RecentStocks'; import { SearchBarContainer, + SearchBarContents, SearchBarInput, SearchBarLayout, - SearchBarResultContainer, - SearchBarResultContent, - SearchBarResultGridContainer, - SearchBarResultItemContainer, - SearchBarResultItemKeyword, - SearchBarResultItemSVG, - SearchBarResultItemSubtitle, - SearchBarResultItemTitle, - SearchBarResultLayout, - SearchBarResultLayoutContainer, - SearchBarResultSVG, - SearchBarResultSubtitle, - SearchBarResultTitle, SearchBarSelectBox, - SearchBarSelectBoxItems, } from './SearchBar.Style'; -const matchCharacters = (query: string, text: string) => { - let queryIndex = 0; - - return [...text].map((char) => ({ - char, - isMatch: query[queryIndex] === char && !!++queryIndex, - })); -}; - -const useBlocker = (shouldBlock: boolean, onBlock: () => void, onAlwaysExecute?: () => void) => { - const navigate = useNavigate(); - const location = useLocation(); - const [previousLocation, setPreviousLocation] = useState(location); - - useEffect(() => { - const handleBeforeUnload = (event: PopStateEvent) => { - if (shouldBlock) { - event.preventDefault(); - navigate(previousLocation, { state: previousLocation.state }); - onBlock(); - } - if (onAlwaysExecute) onAlwaysExecute(); - }; - - window.onpopstate = handleBeforeUnload; - }, [shouldBlock, onBlock, onAlwaysExecute]); - - useEffect(() => { - setPreviousLocation(location); - }, [location]); -}; - -const useComponentFocus = ( - initialState: boolean, - ref: React.RefObject, -): [boolean, React.Dispatch>] => { - const [isFocus, setIsFocus] = useState(initialState); - - useEffect(() => ref.current?.[isFocus ? 'focus' : 'blur'](), [isFocus]); - - return [isFocus, setIsFocus]; -}; - -const useOutsideClick = (callback: () => void) => { - const ref = useRef(null); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (ref.current && !ref.current.contains(event.target as Node)) { - callback(); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [callback]); - - return ref; -}; - -const SearchBarItemsComponent = ({ - type, - category, - resultItems, - handleItemClick, - onItemDelete = () => {}, - searchValue = '', - displayEmpty, +const SearchBar = ({ + initial = { type: 'STOCK', value: '' }, }: { - type: 'SEARCHED' | 'RECENT' | 'POPULAR'; - category: SEARCH_CATEGORY; - resultItems: SearchBarResultItems[]; - handleItemClick: (item: AutoCompleteItem) => void; - onItemDelete?: (item: AutoCompleteItem) => void; - searchValue?: string; - displayEmpty?: boolean; + initial: { type: SearchCategoryKey; value: string }; }) => { - const width = `${type === 'RECENT' ? 50 : 100}%`; - const column = type === 'RECENT' ? 1 : type === 'POPULAR' ? 2 : category === 'STOCK' ? 3 : 2; - const title = - type === 'SEARCHED' ? '검색 결과' : `${type === 'RECENT' ? '최근' : '인기'} 검색 ${SEARCH_CATEGORY_TEXT[category]}`; - - const searchKeyword = resultItems[0]?.keyword; - const isHidden = type === 'RECENT' && !resultItems.length; - - if (isHidden) return; - return ( - - {type === 'POPULAR' || resultItems.length ? ( - <> - {title} - {type === 'SEARCHED' && category == 'KEYWORD' && ( - - {searchKeyword} 이(가) 가장 많이 업급된 종목순으로 노출됩니다. - - )} - - {resultItems.map((item, idx) => { - const { symbolName, symbol, exchangeNum, country, keywordNames, keyword, value } = item; - - return ( -
    - handleItemClick(item)}> - {type == 'POPULAR' && {idx + 1}} - {(category === 'STOCK' ? type === 'RECENT' || type === 'POPULAR' : type === 'SEARCHED') && ( - {STOCK_COUNTRY_TEXT[country]} 종목 - )} - - {type === 'SEARCHED' - ? category === 'STOCK' - ? matchCharacters(searchValue.toLocaleUpperCase(), symbolName).map(({ isMatch, char }, i) => - isMatch ? {char} : char, - ) - : symbolName - : value} - {type === 'SEARCHED' && category === 'STOCK' && ( -
    -
    - {matchCharacters(searchValue.toLocaleUpperCase(), symbol).map(({ isMatch, char }, i) => - isMatch ? ( - {char} - ) : ( - char - ), - )} -
    -
    {MARKET_CODES[exchangeNum]}
    -
    - )} -
    - {type === 'RECENT' && ( - - { - e.preventDefault(); - e.stopPropagation(); - onItemDelete(item); - }} - /> - - )} - {type === 'SEARCHED' && category == 'KEYWORD' && ( - <> - {searchKeyword} - - {keywordNames.filter((e) => e !== keyword)[0]} - - - )} -
    -
    - ); - })} -
    - - ) : ( - displayEmpty && ( - - - - ) - )} -
    - ); -}; - -const SearchBar = () => { const navigate = useNavigate(); - const isMobile = useIsMobile(); - - const SEARCHED_RESULT_MAX_LENGTH = { - STOCK: 15, - KEYWORD: isMobile ? 15 : 10, - }; - - const [inputValue, setInputValue] = useState(''); - const searchValue = inputValue.replace(/\s+/g, '').toUpperCase(); - - const inputRef = useRef(null); - const selectBoxRef = useRef(null); - - const [isActiveSearchBar, setIsActiveSearchBar] = useState(false); - const [isFocusSelectBox, setIsFocusSelectBox] = useComponentFocus(false, selectBoxRef); - const [isFocusInput, setIsFocusInput] = useComponentFocus(false, inputRef); - - const [selectedCategory, setSelectedCategory] = useState('STOCK'); - const [focusedItem, setFocusedItem] = useState({ - idx: -1, - type: '', - }); - focusedItem; - - const resultContainerRef = useRef(null); - const resultContainerHeight = window.innerHeight - (resultContainerRef.current?.getBoundingClientRect().top ?? 0); - - const [recentStocks, setRecentStocks] = useState( - getItemLocalStorage(STORAGE_RECENT_ITEMS['STOCK'], []), - ); - const [recentKeyowrds, setRecentKeyowrds] = useState( - getItemLocalStorage(STORAGE_RECENT_ITEMS['KEYWORD'], []), - ); - const [recentItems, setRecentItems] = - selectedCategory == 'STOCK' ? [recentStocks, setRecentStocks] : [recentKeyowrds, setRecentKeyowrds]; - - const [popularStocks] = usePopularStockFetchQuery(); - const [popularKeywords] = PopularKeywordQuery(); - const popularItems = selectedCategory == 'STOCK' ? popularStocks : popularKeywords; - - const [searchedStocks, setSearchedStocks] = useAutoComplete(fetchAutoComplete, 'symbolName'); - const [searchedKeywords, setSearchedKeywords] = useAutoComplete(fetchSearchKeyword, 'keyword'); - const searchedItems = selectedCategory == 'STOCK' ? searchedStocks : searchedKeywords; - - useEffect(() => { - const setSearchedItems = selectedCategory == 'STOCK' ? setSearchedStocks : setSearchedKeywords; - setSearchedItems(searchValue); - }, [searchValue, selectedCategory]); - - useBlocker( - isMobile && isActiveSearchBar, - () => updateActiveSearchBar(false), - () => inputRef.current?.blur(), - ); - - // const searchBarInputKeyDown = async (e: React.KeyboardEvent) => { - // // const length = searchValue == '' ? (selectedCategory == 'STOCK' ? ()):(2); - - // if (searchValue == '') { - // const recentItems = selectedCategory == 'STOCK' ? recentStocks : recentKeyowrds; - // const popularItems = - // selectedCategory == 'STOCK' ? popularStocks : popularKeywords[selectedCountry]; - // } else { - // const searchedItems = selectedCategory == 'STOCK' ? searchedStocks : searchedKeywords; - // } - // // if (e.key == 'ArrowDown') { - // // e.preventDefault(); - // // setFocusIdx((prev) => - // // stockName == '' ? (prev + 1) % stockSearchInfo.length : (prev + 1) % searchedResult.length, - // // ); - // // } else if (e.key == 'ArrowUp') { - // // e.preventDefault(); - // // setFocusIdx((prev) => - // // stockName == '' - // // ? ((prev == -1 ? 0 : prev) + stockSearchInfo.length - 1) % stockSearchInfo.length - // // : ((prev == -1 ? 0 : prev) + searchedResult.length - 1) % searchedResult.length, - // // ); - // // } + const [searchType, setSearchType] = useState(initial.type); - // // else if (e.key == 'Escape') { - // // (document.activeElement as HTMLElement).blur(); - // // setFocusIdx(-1); - // // } else if (e.key === 'Enter') { - // // if (focusIdx === -1) return; - // // const name = - // // stockName == '' - // // ? stockSearchInfo[focusIdx].symbolName - // // : searchedResult[focusIdx].symbolName; - // // const country = - // // stockName == '' - // // ? stockSearchInfo[focusIdx].country - // // : searchedResult[focusIdx].country; - // // const result = await fetchSearchSymbolName(name, country); - // // if (result) { - // // const curStockSearchInfo: StockSearchInfo = { - // // symbolName: result.symbolName, - // // country: result.country, - // // }; - // // handleSearch(curStockSearchInfo); - // // } - // // } - // }; + const selectedSearchType = SEARCH_CATEGORY_MAP[searchType].text; + const [searchValue, setSearchValue] = useState(initial.value); - const handleSearchValueChange = (e: React.ChangeEvent) => { - const value = e.target.value; - - setInputValue(value); - setFocusedItem((prev: any) => ({ ...prev, idx: -1 })); - }; - - const updateActiveSearchBar = (active: boolean) => { - const onScrollEvent = () => { - function lockScroll() { - window.scrollTo(0, 0); - } - window.requestAnimationFrame(lockScroll); - }; - - if (isMobile) { - document.documentElement.style.overflow = active ? 'hidden' : ''; - document.body.style.overflow = active ? 'hidden' : ''; - document.body.style.position = active ? 'fixed' : ''; // iOS Safari - document.body.style.inset = active ? '0px' : ''; - document.body.style.left = active ? '0px' : ''; - document.body.style.right = active ? '0px' : ''; - window.onscroll = active ? onScrollEvent : null; - - if (active) { - window.scrollTo({ top: 0, behavior: 'smooth' }); - } else { - resultContainerRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); - } - } - setIsActiveSearchBar(active); + const handleSearchTypeChange = (searchType: SearchCategoryKey) => () => { + setSearchType(searchType); }; - // LocalStorage - - const addRecentItem = (item: AutoCompleteItem) => { - const { value, country } = item; - const updatedItems = [item, ...recentItems.filter((e) => e.value !== value || e.country !== country)]; - - setItemLocalStorage(STORAGE_RECENT_ITEMS[selectedCategory], updatedItems); - setRecentItems(updatedItems); - }; - - const deleteRecentItem = (item: AutoCompleteItem) => { - const { value, country } = item; - const updatedItems = recentItems.filter((e) => e.value !== value || e.country !== country); - - setItemLocalStorage(STORAGE_RECENT_ITEMS[selectedCategory], updatedItems); - setRecentItems(updatedItems); - }; - - const handleSearch = (item: AutoCompleteItem) => { - const { symbolName, country } = item; - addRecentItem(item); - navigate(webPath.search(), { state: { symbolName, country } }); - setInputValue(''); - (document.activeElement as HTMLElement).blur(); - updateActiveSearchBar(false); - }; - - // SearchBar EventHandler - - const handleSearchBarInputFocus = () => { - setIsFocusInput(true); - updateActiveSearchBar(true); - }; - - // SelectBox EventHandler - - const handleSelectBoxClick = () => { - if (!isActiveSearchBar && !isFocusSelectBox) updateActiveSearchBar(true); - setIsFocusSelectBox(!isFocusSelectBox); + const handleSearchValueChange = (e: React.ChangeEvent) => { + setSearchValue(e.target.value); }; - const handleSelectBoxItemClick = (category: SEARCH_CATEGORY) => { - setSelectedCategory(category); - setIsFocusSelectBox(false); - setInputValue(''); + const handleSearchValueClear = () => { + setSearchValue(''); }; - const handleItemClick = (item: AutoCompleteItem) => { - if (searchValue.length || selectedCategory == 'STOCK') handleSearch(item); - else setInputValue(item.value); + const handleSearchBarClose = () => { + navigate(-1); }; - // Component - - const searchBarContainerRef = useOutsideClick(() => updateActiveSearchBar(false)); - return ( - <> - - - setIsFocusSelectBox(false)} - tabIndex={-1} - minimize={isFocusInput || (!!inputValue.length && !isFocusSelectBox)} - > -