diff --git a/.vscode/settings.json b/.vscode/settings.json
index c9cd64d..ab13ead 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -44,6 +44,7 @@
"lotties",
"Onesignal",
"Pressable",
- "Pretendard"
+ "Pretendard",
+ "svgs"
]
}
diff --git a/app/(app)/(tabs)/profile.tsx b/app/(app)/(tabs)/profile.tsx
index e631f79..8efd542 100644
--- a/app/(app)/(tabs)/profile.tsx
+++ b/app/(app)/(tabs)/profile.tsx
@@ -9,6 +9,7 @@ import {css} from '@emotion/native';
import {Pressable} from 'react-native';
import {IC_ICON} from '../../../src/icons';
import {openURL} from '../../../src/utils/common';
+import DoobooStats from '../../../src/components/fragments/DoobooStats';
const Container = styled.SafeAreaView`
flex: 1;
@@ -42,13 +43,20 @@ const UserName = styled(Typography.Heading5)`
margin-bottom: 8px;
`;
-const UserBio = styled.Text`
+const UserAffiliation = styled.Text`
font-size: 16px;
color: ${({theme}) => theme.role.secondary};
text-align: center;
margin-bottom: 16px;
`;
+const UserBio = styled.Text`
+ font-size: 16px;
+ color: ${({theme}) => theme.text.label};
+ text-align: center;
+ margin-bottom: 16px;
+`;
+
const InfoCard = styled.View`
background-color: ${({theme}) => theme.bg.paper};
border-radius: 15px;
@@ -72,9 +80,7 @@ const InfoLabel = styled(Typography.Body2)`
font-family: Pretendard-Bold;
`;
-const InfoValue = styled(Typography.Body2)`
- flex: 1;
-`;
+const InfoValue = styled(Typography.Body2)``;
const TagContainer = styled.View`
flex-direction: row;
@@ -114,6 +120,9 @@ export default function Profile(): JSX.Element {
source={user?.avatar_url ? {uri: user?.avatar_url} : IC_ICON}
/>
{user?.display_name || ''}
+ {user?.affiliation ? (
+ {user?.affiliation}
+ ) : null}
{user?.introduction ? {user?.introduction} : null}
@@ -134,10 +143,7 @@ export default function Profile(): JSX.Element {
{user?.github_id || ''}
-
-
- {t('onboarding.affiliation')}
- {user?.affiliation || ''}
+
diff --git a/app/(app)/post/[id]/replies.tsx b/app/(app)/post/[id]/replies.tsx
index d4eca66..7539288 100644
--- a/app/(app)/post/[id]/replies.tsx
+++ b/app/(app)/post/[id]/replies.tsx
@@ -97,8 +97,6 @@ export default function Replies({
})),
});
- console.log('newReply', newReply);
-
if (newReply) {
setReplies((prevReplies) => [newReply, ...prevReplies]);
}
diff --git a/app/(auth)/sign-in.tsx b/app/(auth)/sign-in.tsx
index 5bb19b0..fc82cf9 100644
--- a/app/(auth)/sign-in.tsx
+++ b/app/(auth)/sign-in.tsx
@@ -11,7 +11,7 @@ import {Redirect, Stack, useRouter} from 'expo-router';
import {useRecoilValue} from 'recoil';
import {googleClientIdIOS, googleClientIdWeb} from '../../config';
-import {IC_CROSSPLATFORMS, IC_GOOGLE, IC_ICON} from '../../src/icons';
+import {IMG_CROSSPLATFORMS, IC_GOOGLE, IC_ICON} from '../../src/icons';
import {authRecoilState} from '../../src/recoil/atoms';
import {t} from '../../src/STRINGS';
import {supabase} from '../../src/supabase';
@@ -162,7 +162,7 @@ export default function SignIn(): JSX.Element {
{t('signIn.description')}
=> {
+ try {
+ const response = await fetch(API_ENDPOINT, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({login}),
+ });
+
+ if (!response.ok) {
+ throw new Error('HTTP error! status:' + response.status);
+ }
+
+ return await response.json();
+ } catch (error) {
+ if (__DEV__) console.error('Error fetching data:', error);
+ throw new Error(t('error.failedToFetchData'));
+ }
+};
diff --git a/src/components/fragments/DoobooStats.tsx b/src/components/fragments/DoobooStats.tsx
new file mode 100644
index 0000000..e782f13
--- /dev/null
+++ b/src/components/fragments/DoobooStats.tsx
@@ -0,0 +1,60 @@
+import styled, {css} from '@emotion/native';
+import {useEffect, useState} from 'react';
+import {DoobooGithubStats} from '../../types/github-stats';
+import {updateDoobooGithub} from '../../apis/githubStatsQueries';
+import Scouter from '../uis/Scouter';
+import {User} from '../../types';
+import CustomLoadingIndicator from '../uis/CustomLoadingIndicator';
+
+const Container = styled.View``;
+
+type Props = {
+ user: User | null;
+};
+
+export default function DoobooStats({user}: Props): JSX.Element | null {
+ const [doobooStats, setDoobooStats] = useState(
+ null,
+ );
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const fetchGithubStats = async () => {
+ try {
+ if (!user?.github_id) {
+ return;
+ }
+ const result = await updateDoobooGithub(user!.github_id!);
+
+ if (!!result?.stats) {
+ setDoobooStats(result.stats);
+ }
+ } catch (e: any) {
+ setError(e.message);
+ }
+ };
+
+ if (!!user?.github_id) {
+ fetchGithubStats();
+ }
+ }, [user, user?.github_id]);
+
+ if (error) {
+ return null;
+ }
+
+ return (
+
+ {doobooStats ? (
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/src/components/svgs/SvgStatsDooboo.tsx b/src/components/svgs/SvgStatsDooboo.tsx
new file mode 100644
index 0000000..bd67b40
--- /dev/null
+++ b/src/components/svgs/SvgStatsDooboo.tsx
@@ -0,0 +1,31 @@
+import { useDooboo } from 'dooboo-ui';
+import {Svg, G, Path, Defs, ClipPath, Rect} from 'react-native-svg';
+
+type Props = {
+ color?: string;
+};
+
+export default function SvgStatsDooboo({color}: Props) {
+ const {theme} = useDooboo();
+ const fill = color || theme.text.basic;
+
+ return (
+
+ );
+}
diff --git a/src/components/svgs/SvgStatsEarth.tsx b/src/components/svgs/SvgStatsEarth.tsx
new file mode 100644
index 0000000..fec734d
--- /dev/null
+++ b/src/components/svgs/SvgStatsEarth.tsx
@@ -0,0 +1,30 @@
+import {useDooboo} from 'dooboo-ui';
+import {G, Path, Svg} from 'react-native-svg';
+
+type Props = {
+ color?: string;
+};
+
+export default function SvgStatsEarth({color}: Props) {
+ const {theme} = useDooboo();
+ const fill = color || theme.text.basic;
+
+ return (
+
+ );
+}
diff --git a/src/components/svgs/SvgStatsFire.tsx b/src/components/svgs/SvgStatsFire.tsx
new file mode 100644
index 0000000..e34527b
--- /dev/null
+++ b/src/components/svgs/SvgStatsFire.tsx
@@ -0,0 +1,30 @@
+import {useDooboo} from 'dooboo-ui';
+import {G, Path, Svg} from 'react-native-svg';
+
+type Props = {
+ color?: string;
+};
+
+export default function SvgStatsFire({color}: Props) {
+ const {theme} = useDooboo();
+ const fill = color || theme.text.basic;
+
+ return (
+
+ );
+}
diff --git a/src/components/svgs/SvgStatsGold.tsx b/src/components/svgs/SvgStatsGold.tsx
new file mode 100644
index 0000000..1eeed17
--- /dev/null
+++ b/src/components/svgs/SvgStatsGold.tsx
@@ -0,0 +1,30 @@
+import {useDooboo} from 'dooboo-ui';
+import {G, Path, Svg} from 'react-native-svg';
+
+type Props = {
+ color?: string;
+};
+
+export default function SvgStatsGold({color}: Props) {
+ const {theme} = useDooboo();
+ const fill = color || theme.text.basic;
+
+ return (
+
+ );
+}
diff --git a/src/components/svgs/SvgStatsPerson.tsx b/src/components/svgs/SvgStatsPerson.tsx
new file mode 100644
index 0000000..a7de333
--- /dev/null
+++ b/src/components/svgs/SvgStatsPerson.tsx
@@ -0,0 +1,34 @@
+import {useDooboo} from 'dooboo-ui';
+import {G, Path, Svg} from 'react-native-svg';
+
+type Props = {
+ color?: string;
+};
+
+export default function SvgStatsPerson({color}: Props) {
+ const {theme} = useDooboo();
+ const fill = color || theme.text.basic;
+
+ return (
+
+ );
+}
diff --git a/src/components/svgs/SvgStatsTree.tsx b/src/components/svgs/SvgStatsTree.tsx
new file mode 100644
index 0000000..9c9819e
--- /dev/null
+++ b/src/components/svgs/SvgStatsTree.tsx
@@ -0,0 +1,23 @@
+import {useDooboo} from 'dooboo-ui';
+import {G, Path, Svg} from 'react-native-svg';
+
+type Props = {
+ color?: string;
+};
+
+export default function SvgStatsTree({color}: Props) {
+ const {theme} = useDooboo();
+ const fill = color || theme.text.basic;
+
+ return (
+
+ );
+}
diff --git a/src/components/svgs/SvgStatsWater.tsx b/src/components/svgs/SvgStatsWater.tsx
new file mode 100644
index 0000000..322b674
--- /dev/null
+++ b/src/components/svgs/SvgStatsWater.tsx
@@ -0,0 +1,30 @@
+import {useDooboo} from 'dooboo-ui';
+import {G, Path, Svg} from 'react-native-svg';
+
+type Props = {
+ color?: string;
+};
+
+export default function SvgStatsWater({color}: Props) {
+ const {theme} = useDooboo();
+ const fill = color || theme.text.basic;
+
+ return (
+
+ );
+}
diff --git a/src/components/uis/Scouter/CombatDetails.tsx b/src/components/uis/Scouter/CombatDetails.tsx
new file mode 100644
index 0000000..a9db49e
--- /dev/null
+++ b/src/components/uis/Scouter/CombatDetails.tsx
@@ -0,0 +1,518 @@
+import styled from '@emotion/native';
+import {Typography, useDooboo} from 'dooboo-ui';
+import {type ReactElement} from 'react';
+import {
+ Linking,
+ Platform,
+ Text,
+ TouchableOpacity,
+ View,
+ ViewStyle,
+} from 'react-native';
+
+import {
+ ContentDetailDescProps,
+ DoobooGithubStats,
+ PluginStats,
+ Stats,
+ StatsDetail,
+ StatsElement,
+ StatType,
+} from '../../../types/github-stats';
+import {t} from '../../../STRINGS';
+import SvgStatsDooboo from '../../svgs/SvgStatsDooboo';
+import SvgStatsTree from '../../svgs/SvgStatsTree';
+import SvgStatsEarth from '../../svgs/SvgStatsEarth';
+import SvgStatsFire from '../../svgs/SvgStatsFire';
+import SvgStatsGold from '../../svgs/SvgStatsGold';
+import SvgStatsWater from '../../svgs/SvgStatsWater';
+import SvgStatsPerson from '../../svgs/SvgStatsPerson';
+
+const Container = styled.View`
+ width: ${Platform.OS === 'web' ? 'calc(100vw - 56px)' : undefined};
+ min-width: 300px;
+ border-radius: 4px;
+ border-width: 1px;
+ border-color: ${({theme}) => theme.role.border};
+ align-self: stretch;
+ justify-content: center;
+ overflow: hidden;
+
+ flex-direction: column;
+ align-items: center;
+`;
+
+const Details = styled.View`
+ width: 100%;
+ padding: 20px;
+`;
+
+const DetailHead = styled.View`
+ margin-bottom: 12px;
+
+ flex-direction: row;
+ align-items: center;
+`;
+
+const DetailBody = styled.View`
+ width: 100%;
+
+ flex-direction: column;
+`;
+
+const StatIcons = ({
+ selectedStats,
+ onPressStat,
+}: {
+ selectedStats: StatType | null;
+ onPressStat: (stat: StatType | null) => void;
+}): ReactElement => {
+ const {theme} = useDooboo();
+
+ const style: ViewStyle = {
+ width: 24,
+ height: 24,
+ };
+
+ return (
+
+ onPressStat(null)}
+ style={[style, {padding: 2}]}
+ >
+
+
+ onPressStat('tree')}
+ style={[style, {padding: 2}]}
+ >
+
+
+ onPressStat('fire')}
+ style={[style, {padding: 2}]}
+ >
+
+
+ onPressStat('earth')}
+ style={[style, {padding: 2}]}
+ >
+
+
+ onPressStat('gold')}
+ style={[style, {padding: 2}]}
+ >
+
+
+ onPressStat('water')}
+ style={[style, {padding: 2}]}
+ >
+
+
+ onPressStat('people')}
+ style={[style, {padding: 2}]}
+ >
+
+
+
+ );
+};
+
+const ContentDetailDescription = ({
+ json,
+ stats,
+ selectedStats,
+}: ContentDetailDescProps): ReactElement | null => {
+ const {theme} = useDooboo();
+
+ if (!stats) {
+ return null;
+ }
+
+ const {name, description, statElements, score} = stats;
+
+ const renderCommonDetail = ({
+ statsElement,
+ statsDetails,
+ }: {
+ statsElement: StatsElement;
+ statsDetails?: ReactElement;
+ }): ReactElement => {
+ return (
+
+
+
+
+ {statsElement.name}
+
+
+
+
+ {statsElement.totalCount?.toLocaleString() || ''}
+
+
+
+ {statsDetails}
+
+ );
+ };
+
+ const renderDetails = (
+ details: StatsDetail[],
+ statsElement: StatsElement,
+ ) => {
+ switch (details.length) {
+ case 0:
+ return renderCommonDetail({statsElement});
+ case 1:
+ return renderCommonDetail({
+ statsElement,
+ statsDetails: (
+
+ {details.map((detail: StatsDetail, i) => {
+ switch (detail.type) {
+ case 'repository':
+ return (
+
+ Linking.openURL(detail.url)}
+ >
+ {detail.name}
+
+ {i < details.length - 1 ? , : null}
+
+ );
+ case 'commit':
+ return (
+
+
+
+ Linking.openURL(
+ `https://github.com/${detail.name}`,
+ )
+ }
+ >
+ {detail.name}
+
+
+
+
+ Linking.openURL(
+ `https://github.com/${detail.name}/commit/${detail.sha}`,
+ )
+ }
+ >
+ {`${detail.message}`}
+
+
+
+ );
+ case 'language':
+ return (
+
+
+
+ {detail.name}
+
+
+
+
+ {detail.count.toLocaleString()}
+
+
+
+ );
+ }
+ })}
+
+ ),
+ });
+ default: // Multiple details in one attr
+ return renderCommonDetail({
+ statsElement,
+ statsDetails: (
+
+ {details.map((detail: StatsDetail, i) => {
+ switch (detail.type) {
+ case 'repository':
+ return (
+
+ Linking.openURL(detail.url)}
+ >
+ {detail.name}
+
+ {i < details.length - 1 ? , : null}
+
+ );
+ case 'commit':
+ return (
+
+
+
+ Linking.openURL(
+ `https://github.com/${detail.name}`,
+ )
+ }
+ >
+ {detail.name}
+
+
+ Linking.openURL(
+ `https://github.com/${detail.name}/commit/${detail.sha}`,
+ )
+ }
+ >
+ {`${detail.message}`}
+
+
+
+ );
+ case 'language':
+ return (
+
+
+
+ {detail.name}
+
+
+
+
+ {detail.count.toLocaleString()}
+
+
+
+ );
+ }
+ })}
+
+ ),
+ });
+ }
+ };
+
+ if (!selectedStats) {
+ return (
+
+
+
+ {t('common.bio')}
+
+
+
+
+ {json.bio}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {name}{' '}
+
+ ({t('common.score')}: {Math.round(score * 100)})
+
+
+
+ {description ? (
+
+ {description}
+
+ ) : null}
+
+ {statElements?.map((el: StatsElement) => {
+ if (!el.name) {
+ return null;
+ }
+
+ const details: StatsDetail[] = el.details
+ ? JSON.parse(el.details)
+ : [];
+
+ return {renderDetails(details, el)};
+ })}
+
+
+ );
+};
+
+export type Props = {
+ json: DoobooGithubStats['json'];
+ pluginStats: PluginStats;
+ selectedStat?: StatType | null;
+ onPressStat: (type: StatType | null) => void;
+};
+
+const CombatDetails = ({
+ selectedStat = 'tree',
+ pluginStats,
+ onPressStat,
+ json,
+}: Props): ReactElement => {
+ const stats: Stats =
+ selectedStat === 'tree'
+ ? pluginStats.tree
+ : selectedStat === 'fire'
+ ? pluginStats.fire
+ : selectedStat === 'earth'
+ ? pluginStats.earth
+ : selectedStat === 'gold'
+ ? pluginStats.gold
+ : selectedStat === 'water'
+ ? pluginStats.water
+ : pluginStats.people;
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default CombatDetails;
diff --git a/src/components/uis/Scouter/Score.tsx b/src/components/uis/Scouter/Score.tsx
new file mode 100644
index 0000000..1893041
--- /dev/null
+++ b/src/components/uis/Scouter/Score.tsx
@@ -0,0 +1,101 @@
+import styled from '@emotion/native';
+import {type ReactElement} from 'react';
+import {
+ IC_TIER_BRONZE,
+ IC_TIER_CHALLENGER,
+ IC_TIER_DIAMOND,
+ IC_TIER_GOLD,
+ IC_TIER_MASTER,
+ IC_TIER_PLATINUM,
+ IC_TIER_SILVER,
+} from '../../../icons';
+
+const Container = styled.View`
+ align-self: stretch;
+ justify-content: center;
+
+ flex-direction: row;
+ align-items: center;
+`;
+
+const TierView = styled.View`
+ min-width: 90px;
+
+ flex-direction: row;
+ align-items: center;
+`;
+
+const ScoreView = styled.View`
+ min-width: 64px;
+ margin-left: 100px;
+
+ flex-direction: row;
+ align-items: center;
+`;
+
+const StyledImage = styled.Image`
+ width: 32px;
+ height: 32px;
+ margin-right: 8px;
+`;
+
+const LabelText = styled.Text`
+ font-size: 12px;
+ opacity: 0.5;
+ color: ${({theme}) => theme.text.basic};
+`;
+
+const ValueText = styled.Text`
+ margin-left: 8px;
+ font-size: 24px;
+ font-weight: bold;
+ color: ${({theme}) => theme.text.basic};
+`;
+
+export type ScoreType = {
+ tierName:
+ | 'Iron'
+ | 'Bronze'
+ | 'Silver'
+ | 'Gold'
+ | 'Platinum'
+ | 'Master'
+ | 'Diamond'
+ | 'Challenger';
+ score: number;
+};
+
+const Score = ({tierName, score = 0}: ScoreType): ReactElement => {
+ return (
+
+
+
+ {tierName}
+
+
+ AVG
+ {score}
+
+
+ );
+};
+
+export default Score;
diff --git a/src/components/uis/Scouter/StatsChart.tsx b/src/components/uis/Scouter/StatsChart.tsx
new file mode 100644
index 0000000..c6f6aba
--- /dev/null
+++ b/src/components/uis/Scouter/StatsChart.tsx
@@ -0,0 +1,269 @@
+import Svg, {Defs, LinearGradient, Polygon, Stop} from 'react-native-svg';
+import {IMG_SPIDER_WEB_LIGHT, IMG_SPIDER_WEB_DARK} from '../../../icons';
+import Animated, {BounceIn} from 'react-native-reanimated';
+
+import {type ReactElement} from 'react';
+import styled from '@emotion/native';
+import {useDooboo} from 'dooboo-ui';
+import {ImageBackground, Platform, TouchableOpacity} from 'react-native';
+import {StatType} from '../../../types/github-stats';
+import SvgStatsPerson from '../../svgs/SvgStatsPerson';
+import SvgStatsTree from '../../svgs/SvgStatsTree';
+import SvgStatsFire from '../../svgs/SvgStatsFire';
+import SvgStatsEarth from '../../svgs/SvgStatsEarth';
+import SvgStatsGold from '../../svgs/SvgStatsGold';
+import SvgStatsWater from '../../svgs/SvgStatsWater';
+
+type Axis = {x: number; y: number};
+
+const Container = styled.View`
+ padding: 32px;
+
+ justify-content: center;
+ align-items: center;
+`;
+
+const StatsContainer = styled.View`
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 4px;
+`;
+
+const convertPosition = (
+ centerPosition: Axis,
+ percentage: number,
+ type: StatType,
+): string => {
+ const toString = (axis: Axis): string => `${axis.x},${axis.y}`;
+
+ let threshold: Axis = centerPosition;
+ let thresholdAxis: Axis = {
+ x: threshold.x * percentage + centerPosition.x,
+ y: threshold.y * percentage + centerPosition.y,
+ };
+ let newCenterPosition: Axis = centerPosition;
+
+ switch (type) {
+ case 'tree':
+ newCenterPosition = {
+ x: centerPosition.x + centerPosition.x * 0.18,
+ y: -centerPosition.y * -0.9,
+ };
+
+ threshold = {
+ x: centerPosition.x,
+ y: -centerPosition.y * 0.5,
+ };
+ break;
+
+ case 'fire':
+ newCenterPosition = {
+ x: centerPosition.x + centerPosition.x * 0.18,
+ y: centerPosition.y + centerPosition.y * 0.5 * 0.18,
+ };
+
+ threshold = {
+ x: centerPosition.x,
+ y: centerPosition.y * 0.5,
+ };
+ break;
+ case 'earth':
+ newCenterPosition = {
+ x: centerPosition.x,
+ y: centerPosition.y + centerPosition.y * 0.18,
+ };
+
+ threshold = {x: 0, y: centerPosition.y};
+ break;
+ case 'gold':
+ newCenterPosition = {
+ x: centerPosition.x + -(centerPosition.x * 0.18),
+ y: centerPosition.y + centerPosition.y * 0.5 * 0.18,
+ };
+
+ threshold = {x: -centerPosition.x, y: centerPosition.y * 0.5};
+ break;
+ case 'water':
+ newCenterPosition = {
+ x: centerPosition.x + -(centerPosition.x * 0.18),
+ y: centerPosition.y + -(centerPosition.y * 0.5 * 0.18),
+ };
+
+ threshold = {x: -centerPosition.x, y: -centerPosition.y * 0.5};
+ break;
+ case 'people':
+ newCenterPosition = {
+ x: centerPosition.x,
+ y: centerPosition.y + -(centerPosition.y * 0.18),
+ };
+
+ threshold = {x: 0, y: -centerPosition.y};
+ break;
+ default:
+ return toString(thresholdAxis);
+ }
+
+ const newPercentage = (80 * percentage) / 100;
+
+ thresholdAxis = {
+ x: threshold.x * newPercentage + newCenterPosition.x,
+ y: threshold.y * newPercentage + newCenterPosition.y,
+ };
+
+ return toString(thresholdAxis);
+};
+
+const StatUnits = ({
+ centerPosition,
+ onPressStat,
+}: {
+ centerPosition: Axis;
+ onPressStat: (type: StatType) => void;
+}): ReactElement => {
+ return (
+ <>
+ onPressStat('people')}
+ style={{
+ position: 'absolute',
+ top: 2,
+ padding: 4,
+ }}
+ >
+
+
+ onPressStat('tree')}
+ style={{
+ position: 'absolute',
+ top: centerPosition.y * 0.7 - 4,
+ right: 2,
+ padding: 4,
+ }}
+ >
+
+
+ onPressStat('fire')}
+ style={{
+ position: 'absolute',
+ bottom: centerPosition.y * 0.7 - 4,
+ right: 2,
+ padding: 4,
+ }}
+ >
+
+
+ onPressStat('earth')}
+ style={{
+ position: 'absolute',
+ bottom: 2,
+ padding: 4,
+ }}
+ >
+
+
+ onPressStat('gold')}
+ style={{
+ position: 'absolute',
+ bottom: centerPosition.y * 0.7 - 4,
+ left: 2,
+ padding: 4,
+ }}
+ >
+
+
+ onPressStat('water')}
+ style={{
+ position: 'absolute',
+ top: centerPosition.y * 0.7 - 4,
+ left: 2,
+ padding: 4,
+ }}
+ >
+
+
+ >
+ );
+};
+
+export type StatsScore = {
+ tree: number;
+ fire: number;
+ earth: number;
+ gold: number;
+ water: number;
+ people: number;
+};
+
+export type StatsChartType = {
+ selectedStat?: StatType | null;
+ statsScore: StatsScore;
+ width?: number;
+ centerPosition?: Axis;
+ onPressStat: (type: StatType) => void;
+};
+
+const AnimatedSvg =
+ Platform.OS === 'web' ? Svg : Animated.createAnimatedComponent(Svg);
+
+const StatsChart = ({
+ selectedStat,
+ statsScore: {tree, fire, earth, gold, water, people},
+ width = 262,
+ centerPosition = {x: width / 2, y: (width * 1.155) / 2},
+ onPressStat,
+}: StatsChartType): ReactElement => {
+ const {theme, themeType} = useDooboo();
+ const height = width * 1.155;
+
+ const posFire = convertPosition(centerPosition, fire, 'fire');
+ const posEarth = convertPosition(centerPosition, earth, 'earth');
+ const posGold = convertPosition(centerPosition, gold, 'gold');
+ const posWater = convertPosition(centerPosition, water, 'water');
+ const posPerson = convertPosition(centerPosition, people, 'people');
+ const posTree = convertPosition(centerPosition, tree, 'tree');
+
+ return (
+
+
+
+
+
+
+
+ {/* @ts-ignore - This will be fixed in react-native-svg@13+*/}
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default StatsChart;
diff --git a/src/components/uis/Scouter/index.tsx b/src/components/uis/Scouter/index.tsx
new file mode 100644
index 0000000..5f3a53d
--- /dev/null
+++ b/src/components/uis/Scouter/index.tsx
@@ -0,0 +1,111 @@
+import {memo, useEffect, useState, type ReactElement} from 'react';
+import styled from '@emotion/native';
+import StatsChart, {StatsChartType} from './StatsChart';
+import {StyleProp, View, ViewStyle} from 'react-native';
+import {
+ DoobooGithubStats,
+ StatType,
+ TierType,
+} from '../../../types/github-stats';
+import Score, {ScoreType} from './Score';
+import {DEFAULT_GITHUB_STATS} from '../../../utils/constants';
+import CombatDetails from './CombatDetails';
+
+const Container = styled.View`
+ flex-direction: column;
+ align-items: center;
+`;
+
+const Scouter = ({
+ githubLogin,
+ doobooStats,
+ chartType,
+ style,
+}: {
+ githubLogin?: string | null;
+ doobooStats: DoobooGithubStats;
+ chartType?: Omit<
+ StatsChartType,
+ 'statsScore' | 'onPressStat' | 'selectedStat'
+ >;
+ style?: StyleProp;
+}): ReactElement => {
+ const [selectedStat, setSelectedStat] = useState(null);
+ const [tierName, setTierName] = useState('Silver');
+ const pluginStats = !githubLogin
+ ? DEFAULT_GITHUB_STATS.pluginStats
+ : doobooStats.pluginStats;
+
+ const sum =
+ +pluginStats.earth.score +
+ +pluginStats.fire.score +
+ +pluginStats.gold.score +
+ +pluginStats.people.score +
+ +pluginStats.tree.score +
+ +pluginStats.water.score;
+
+ const score = Math.round((sum * 100) / 6);
+
+ useEffect(() => {
+ if (githubLogin && doobooStats.plugin.json) {
+ const tierJSONArray: TierType[] = JSON.parse(
+ JSON.stringify(doobooStats.plugin.json),
+ );
+
+ const tiers = tierJSONArray.filter((el) => el.score <= score);
+ if (tiers.length === 0) {
+ setTierName('Iron');
+
+ return;
+ }
+
+ setTierName(tiers[tiers.length - 1].tier as ScoreType['tierName']);
+ }
+ }, [doobooStats.plugin.json, githubLogin, score]);
+
+ const onPressStat = (stat: StatType | null): void => {
+ setSelectedStat(stat);
+ };
+
+ return (
+
+
+ {githubLogin ? (
+
+
+
+
+
+
+
+
+ ) : null}
+
+
+
+ );
+};
+
+export default memo(Scouter);
diff --git a/src/icons.ts b/src/icons.ts
index 5d936f3..b0e643f 100644
--- a/src/icons.ts
+++ b/src/icons.ts
@@ -1,9 +1,28 @@
import ICON from '../assets/icon.png';
-import CROSSPLATFORMS from '../assets/icons/crossplatforms.png';
import GOOGLE from '../assets/icons/google.png';
-import MASK from '../assets/icons/mask.png';
+import CrossPlatforms from '../assets/images/crossplatforms.png';
+import SpiderWebDark from '../assets/images/spider_web_d.png';
+import SpiderWebLight from '../assets/images/spider_web_l.png';
+import TierBronze from '../assets/icons/tier_bronze.png';
+import TierChallenger from '../assets/icons/tier_challenger.png';
+import TierDiamond from '../assets/icons/tier_diamond.png';
+import TierGold from '../assets/icons/tier_gold.png';
+import TierIron from '../assets/icons/tier_iron.png';
+import TierMaster from '../assets/icons/tier_master.png';
+import TierPlatinum from '../assets/icons/tier_platinum.png';
+import TierSilver from '../assets/icons/tier_silver.png';
-export const IC_MASK = MASK;
export const IC_ICON = ICON;
export const IC_GOOGLE = GOOGLE;
-export const IC_CROSSPLATFORMS = CROSSPLATFORMS;
+export const IC_TIER_BRONZE = TierBronze;
+export const IC_TIER_CHALLENGER = TierChallenger;
+export const IC_TIER_DIAMOND = TierDiamond;
+export const IC_TIER_GOLD = TierGold;
+export const IC_TIER_IRON = TierIron;
+export const IC_TIER_MASTER = TierMaster;
+export const IC_TIER_PLATINUM = TierPlatinum;
+export const IC_TIER_SILVER = TierSilver;
+
+export const IMG_CROSSPLATFORMS = CrossPlatforms;
+export const IMG_SPIDER_WEB_DARK = SpiderWebDark;
+export const IMG_SPIDER_WEB_LIGHT = SpiderWebLight;
diff --git a/src/types/github-stats.ts b/src/types/github-stats.ts
new file mode 100644
index 0000000..dc66da0
--- /dev/null
+++ b/src/types/github-stats.ts
@@ -0,0 +1,97 @@
+export type StatsDetail =
+ | {
+ type: 'repository';
+ name: string;
+ url: string;
+ }
+ | {
+ type: 'language';
+ name: string;
+ count: number;
+ }
+ | {
+ type: 'commit';
+ name: string;
+ message: string;
+ comment_count: number;
+ sha: string;
+ score: number;
+ author: string;
+ url: string;
+ };
+
+export type StatsElement = {
+ key: string;
+ name: string;
+ description: string;
+ totalCount: number;
+ details?: string;
+};
+
+export type Stats = {
+ description: string;
+ icon: string;
+ id: string;
+ name: string;
+ score: number;
+ statElements: StatsElement[];
+};
+
+export type Plugin = {
+ name: string;
+ apiURL: string;
+ description?: string;
+ json?: any[];
+};
+
+export type TierType = {
+ tier: string;
+ score: number;
+};
+
+export type PluginStats = {
+ earth: Stats;
+ fire: Stats;
+ gold: Stats;
+ people: Stats;
+ tree: Stats;
+ water: Stats;
+ dooboo: Stats;
+};
+
+export type DoobooGithubStats = {
+ json: {
+ login: string;
+ avatarUrl?: string;
+ bio?: string;
+ score?: number;
+ languages?: {name: string; color: string; size: number}[];
+ };
+ plugin: Plugin;
+ pluginStats: PluginStats;
+};
+
+export type StatType = 'tree' | 'fire' | 'earth' | 'gold' | 'water' | 'people';
+
+export type StatsElementType = {
+ key: string;
+ name: string;
+ description: string;
+ totalCount: number;
+ details: string;
+};
+
+export type StatsElements = {
+ tree: StatsElementType[];
+ fire: StatsElementType[];
+ earth: StatsElementType[];
+ gold: StatsElementType[];
+ water: StatsElementType[];
+ people: StatsElementType[];
+};
+
+export type ContentDetailDescProps = {
+ json: DoobooGithubStats['json'];
+ stats: Stats;
+ selectedStats: StatType | null;
+};
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index ec25c25..0eccca5 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -1,3 +1,5 @@
+import {DoobooGithubStats, Stats} from '../types/github-stats';
+
export const LIST_CNT = 10;
export const AsyncStorageKey = {
@@ -12,4 +14,32 @@ export const PAGE_SIZE = 10;
export const HEADER_HEIGHT = 56;
export const MAX_IMAGES_UPLOAD_LENGTH = 5;
export const MAX_WIDTH = 1000;
-export const EMAIL_ADDRESS = 'crossplatformkorea@gmail.com';
\ No newline at end of file
+export const EMAIL_ADDRESS = 'crossplatformkorea@gmail.com';
+
+export const initStats: Stats = {
+ name: '',
+ description: '',
+ score: 0.0,
+ statElements: [],
+ icon: '',
+ id: '',
+};
+
+export const DEFAULT_GITHUB_STATS: DoobooGithubStats = {
+ json: {
+ login: '',
+ },
+ pluginStats: {
+ earth: initStats,
+ fire: initStats,
+ gold: initStats,
+ people: initStats,
+ tree: initStats,
+ water: initStats,
+ dooboo: initStats,
+ },
+ plugin: {
+ name: 'dooboo-github',
+ apiURL: '',
+ },
+};