generated from idea2app/Next-Bootstrap-ts
-
Notifications
You must be signed in to change notification settings - Fork 6
[add] Wealth Product selector page #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from 2 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
4162281
feat: Add finance MVP with AkShare data service and landing page
dethan3 f8ca608
[fix] PNPM lock dismatch
TechQuery 3a1f964
feat(finance): internationalize finance page and fund cards
dethan3 4d89463
[refactor] split Spark Line component into an independent module
TechQuery 712096f
[optimize] generate Next.js types automatically
TechQuery c1ca5c7
chore: document /finance page in README and remove finance PRD
dethan3 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| import { FC, useMemo } from 'react'; | ||
| import { Badge, Card } from 'react-bootstrap'; | ||
|
|
||
| import { INDEX_CATEGORY_LABELS, INDEX_RISK_LABELS } from '../../constants/finance'; | ||
| import styles from '../../styles/Finance.module.scss'; | ||
| import { IndexFundSnapshot, IndexHistoryPoint } from '../../types/finance'; | ||
|
|
||
| export interface FundCardProps { | ||
| data: IndexFundSnapshot; | ||
| } | ||
|
|
||
| const formatNumber = (value: number | null | undefined) => | ||
| value == null ? '--' : value.toLocaleString('zh-CN', { maximumFractionDigits: 2 }); | ||
|
|
||
| const formatPercent = (value: number | null | undefined) => | ||
| value == null ? '--' : `${(value * 100).toFixed(2)}%`; | ||
|
|
||
| const valueTone = (value: number | null | undefined) => | ||
| value == null ? styles.mutedText : value >= 0 ? styles.positiveText : styles.negativeText; | ||
dethan3 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const Sparkline: FC<{ points: IndexHistoryPoint[]; chartId: string }> = ({ points, chartId }) => { | ||
| const { polyline, gradientStops } = useMemo(() => { | ||
| if (!points.length) return { polyline: '', gradientStops: [] as number[] }; | ||
|
|
||
| const values = points.map(point => point.value); | ||
dethan3 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const min = Math.min(...values); | ||
| const max = Math.max(...values); | ||
| const delta = max - min || 1; | ||
|
|
||
| const polylinePoints = points | ||
| .map((point, index) => { | ||
| const x = (index / Math.max(points.length - 1, 1)) * 100; | ||
| const y = ((max - point.value) / delta) * 40; | ||
dethan3 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return `${x.toFixed(2)},${y.toFixed(2)}`; | ||
| }) | ||
| .join(' '); | ||
|
|
||
| const gradientOffsets = [0, 50, 100]; | ||
|
|
||
| return { polyline: polylinePoints, gradientStops: gradientOffsets }; | ||
| }, [points]); | ||
|
|
||
| if (!points.length) return <div className={styles.sparklinePlaceholder}>数据准备中</div>; | ||
dethan3 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
dethan3 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const gradientId = `${chartId}-gradient`; | ||
|
|
||
| return ( | ||
| <div className={styles.sparkline}> | ||
| <svg viewBox="0 0 100 40" role="img" aria-label="近 60 日走势"> | ||
dethan3 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| <defs> | ||
| <linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%"> | ||
| {gradientStops.map(offset => ( | ||
| <stop | ||
| key={offset} | ||
| offset={`${offset}%`} | ||
| stopColor="var(--bs-primary)" | ||
| stopOpacity="0.5" | ||
| /> | ||
| ))} | ||
| </linearGradient> | ||
| </defs> | ||
| <polyline | ||
| fill="none" | ||
| stroke={`url(#${gradientId})`} | ||
| strokeWidth="2" | ||
| strokeLinejoin="round" | ||
| strokeLinecap="round" | ||
| points={polyline} | ||
| /> | ||
| </svg> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export const FundCard: FC<FundCardProps> = ({ data }) => { | ||
| const { | ||
| displayName, | ||
| category, | ||
| riskLevel, | ||
| latestValue, | ||
| dailyChangePct, | ||
| oneYearReturnPct, | ||
| maxDrawdownPct, | ||
| tags, | ||
| sparkline, | ||
| updatedAt, | ||
| fallback, | ||
| description, | ||
| } = data; | ||
dethan3 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const sparklineId = useMemo(() => `fund-${data.symbol}`, [data.symbol]); | ||
|
|
||
| return ( | ||
| <Card className={`${styles.fundCard} h-100`}> | ||
| <Card.Body className="d-flex flex-column gap-3"> | ||
| <div | ||
| className={`${styles.fundCardHeader} d-flex justify-content-between align-items-start`} | ||
| > | ||
| <div> | ||
| <h3 className="h5 mb-1">{displayName}</h3> | ||
| <div className="d-flex flex-wrap gap-2 align-items-center"> | ||
| <Badge bg="light" text="dark"> | ||
| {INDEX_CATEGORY_LABELS[category]} | ||
| </Badge> | ||
| <Badge | ||
| bg={ | ||
| riskLevel === 'aggressive' | ||
| ? 'danger' | ||
| : riskLevel === 'balanced' | ||
| ? 'warning' | ||
| : 'success' | ||
| } | ||
| > | ||
| {INDEX_RISK_LABELS[riskLevel]} | ||
| </Badge> | ||
| {fallback && ( | ||
| <Badge bg="secondary" text="light"> | ||
| 离线数据 | ||
| </Badge> | ||
| )} | ||
dethan3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </div> | ||
| </div> | ||
| <small className="text-muted text-end"> | ||
| 数据源 <br /> | ||
| {data.source.historyEndpoint} | ||
dethan3 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| </small> | ||
dethan3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </div> | ||
|
|
||
| <p className="text-muted mb-0">{description}</p> | ||
|
|
||
| <div className={`${styles.metricRow} d-flex flex-wrap gap-4 align-items-center`}> | ||
| <div> | ||
| <p className="text-muted mb-1">最新点位</p> | ||
| <span className={styles.metricValue}>{formatNumber(latestValue)}</span> | ||
| </div> | ||
dethan3 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| <div> | ||
| <p className="text-muted mb-1">日涨跌</p> | ||
| <strong className={valueTone(dailyChangePct)}>{formatPercent(dailyChangePct)}</strong> | ||
| </div> | ||
| <div> | ||
| <p className="text-muted mb-1">近 1 年收益</p> | ||
| <strong className={valueTone(oneYearReturnPct)}> | ||
| {formatPercent(oneYearReturnPct)} | ||
| </strong> | ||
| </div> | ||
| <div> | ||
| <p className="text-muted mb-1">最大回撤</p> | ||
| <strong className={valueTone(maxDrawdownPct)}>{formatPercent(maxDrawdownPct)}</strong> | ||
| </div> | ||
| </div> | ||
dethan3 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| <Sparkline points={sparkline} chartId={sparklineId} /> | ||
|
|
||
| <div className="d-flex flex-wrap gap-2"> | ||
| {tags?.map(tag => ( | ||
| <Badge key={tag} bg="light" text="dark" className={styles.tagBadge}> | ||
dethan3 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| {tag} | ||
| </Badge> | ||
| ))} | ||
| </div> | ||
|
|
||
| <div className="d-flex justify-content-between align-items-center"> | ||
| <small className="text-muted">更新于 {updatedAt || '--'}</small> | ||
dethan3 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| <a href={`/finance/${data.symbol}`} className="text-primary fw-semibold"> | ||
| 查看详情 → | ||
| </a> | ||
| </div> | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| </Card.Body> | ||
| </Card> | ||
| ); | ||
| }; | ||
dethan3 marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { IndexFundCategory, IndexRiskLevel } from '../types/finance'; | ||
|
|
||
| export const INDEX_CATEGORY_LABELS: Record<IndexFundCategory, string> = { | ||
| broad: '宽基', | ||
| sector: '行业', | ||
| theme: '主题', | ||
| }; | ||
|
|
||
| export const INDEX_RISK_LABELS: Record<IndexRiskLevel, string> = { | ||
| conservative: '保守', | ||
| balanced: '稳健', | ||
| aggressive: '进取', | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| # 理财产品页面 PRD(国内指数基金精选) | ||
|
|
||
| ## 1. 背景与目标 | ||
|
|
||
| - 平台当前缺少针对理财小白的落地页面,用户难以理解「指数基金」这一资产类别并做出选择。 | ||
| - 国内指数基金数量众多,缺乏以风险/波动友好视角进行筛选的工具。 | ||
| - 目标:推出一个一屏解释、一屏筛选的理财产品页面,帮助用户基于客观数据挑选国内主流指数基金,并引导至基金详情或购买渠道。 | ||
|
|
||
| ## 2. 用户画像 | ||
|
|
||
| | 用户类型 | 需求 | 痛点 | | ||
| | ----------------------------- | -------------------------------- | ------------------------------- | | ||
| | 新手理财用户(25-35 岁白领) | 想开始定投,但不了解指数基金差异 | 术语过多、没有直观风险提示 | | ||
| | 进阶用户(有 1-3 年基金经验) | 需要快速对比指数基金表现 | 需要最新净值/回撤数据和多维排序 | | ||
| | 内容消费用户 | 想要理解指数基金逻辑 | 缺乏简单的知识卡片和案例 | | ||
|
|
||
| ## 3. 使用场景 | ||
|
|
||
| - **首次进入:** 通过首页 Banner/文章跳转到理财产品页,先阅读指数基金为何适合长期定投,再挑选产品。 | ||
| - **每日查看:** 用户关注 2-3 只指数基金,查看实时涨跌、近期表现及风险信号。 | ||
| - **策略对比:** 用户在页面内对比宽基 vs 行业指数,组合不同风险等级产品。 | ||
|
|
||
| ## 4. 信息架构与模块 | ||
|
|
||
| 1. **Hero 讲解区**:一句话卖点 + 国内指数基金长期收益对比图 + CTA(开始挑选)。 | ||
| 2. **筛选器**:分类(宽基/行业/主题)、风险级别、跟踪误差、成立年限、基金规模、费率。 | ||
| 3. **指数基金卡片列表**:每张卡片包含基金简称、跟踪指数、近 1/3/5 年年化、最大回撤、规模、费率、风险标签、趋势小图。 | ||
| 4. **精选组合推荐**:根据风险偏好自动推荐 3 套组合(保守/平衡/进取)。 | ||
| 5. **教育内容区**:FAQ、指数基金 vs 主动基金、定投策略提示。 | ||
| 6. **CTA 区**:跳转到基金详情页、模拟组合或第三方购买链接。 | ||
|
|
||
| ## 5. MVP 范围 | ||
|
|
||
| - 覆盖 30-50 个国内核心指数基金(宽基 + 主要行业指数)。 | ||
| - 筛选功能支持「风险等级」「年化收益」「最大回撤」排序与多条件过滤。 | ||
| - 每只基金提供历史净值曲线(近 5 年)、实时涨跌幅及跟踪指数概览。 | ||
| - 集成 AKShare 指数数据 API,支持每日自动更新和历史回测展示。 | ||
|
|
||
| ## 6. 数据源与技术方案 | ||
|
|
||
| ### 6.1 数据来源:AKShare 指数数据 API | ||
|
|
||
| | 功能 | AKShare 接口 | 关键字段 | 使用说明 | | ||
| | ------------------------- | ------------------------------ | ---------------------------------------------- | ---------------------------------------------------------------------------------------- | | ||
| | 实时指数行情(宽基/行业) | `stock_zh_index_spot_em` | 代码、名称、最新价、涨跌幅、成交额、振幅、量比 | 传入 `symbol="上证系列指数"` / `"深证系列指数"` 获取国内指数实时行情;用于列表实时价格。 | | ||
| | 新浪实时指数补充 | `stock_zh_index_spot_sina` | 名称、最新价、涨跌额、成交量 | 兜底数据源,避免单一供应商不可用。 | | ||
| | 历史日线 | `stock_zh_index_daily_em` | 日期、开收高低、成交量、成交额、换手率 | 用于绘制近 1/3/5 年走势图、计算年化收益/回撤。 | | ||
| | 深度历史(中证指数) | `stock_zh_index_hist_csindex` | 日期、收盘、涨跌幅、换手率、PE、PB | 适配 2005 年前/稀有指数历史,支持长周期回测。 | | ||
| | 指数估值 | `stock_zh_index_value_csindex` | PE、PB、股息率、ROE | 用于显示估值温度计,作为风险提示。 | | ||
|
|
||
| > 若需要关联具体基金,可与「公募基金数据」章节中的 `fund_etf_spot_em`、`fund_open_fund_daily_em` 结合,建立「基金 ↔ 跟踪指数」映射表。 | ||
|
|
||
| ### 6.2 数据流程 | ||
|
|
||
| 1. 数据同步服务(CRON 或 serverless job)调用 AKShare 接口,存入 `index_snapshots`(实时)和 `index_history`(日线)表。 | ||
| 2. 数据层计算指标:近 n 年年化收益、最大/最近回撤、波动率、夏普比率、跟踪误差(需要基金净值数据)。 | ||
| 3. API 层提供 `/api/finance/index-funds`(列表 + 筛选)和 `/api/finance/index-funds/{id}`(详情)两个端点,前端页面直接消费。 | ||
| 4. 数据更新频率:实时行情每 1 分钟刷新、历史日线每日收盘后更新、估值数据每日 23:00 同步。 | ||
|
|
||
| ### 6.3 数据字段映射(示例) | ||
|
|
||
| | 页面字段 | 计算逻辑 | 数据来源 | | ||
| | --------------- | ------------------------------------------------------- | ------------------------------ | | ||
| | 最新净值/点位 | `最新价` | `stock_zh_index_spot_em` | | ||
| | 近 1/3/5 年年化 | CAGR(`stock_zh_index_daily_em` 收盘价) | 历史日线 | | ||
| | 最大回撤 | 以 `stock_zh_index_daily_em` 收盘价计算 | 历史日线 | | ||
| | 风险等级 | 基于波动率、最大回撤阈值映射(例:波动率 < 15% → 保守) | 数据计算层 | | ||
| | 估值温度计 | 当前 PE/PB vs 历史百分位 | `stock_zh_index_value_csindex` | | ||
|
|
||
| ## 7. 功能需求 | ||
|
|
||
| ### 7.1 Hero/教育区 | ||
|
|
||
| - 文案:「长期定投指数基金,抓住经济增长红利」。 | ||
| - 可切换两张图表(沪深 300 vs 中证 500 累计收益)。 | ||
|
|
||
| ### 7.2 筛选器 | ||
|
|
||
| - **类别**:宽基(沪深300、中证500、创业板指)、行业(消费、科技、医药等)、主题。 | ||
| - **风险等级**:保守(年化波动<12%)、稳健(12%-20%)、进取(>20%)。 | ||
| - **更多条件**:成立年限、基金规模、管理费、跟踪误差、近一年回撤。 | ||
| - 支持「预设组合」快速按钮(保守定投/成长定投)。 | ||
|
|
||
| ### 7.3 基金卡片 | ||
|
|
||
| - 顶部显示基金简称 + 基金代码 + 「指数/ETF」标签。 | ||
| - 中部展示:最新价、日涨跌、近 1/3/5 年年化(可切换)、最大回撤、估值温度条。 | ||
| - 底部 CTA:「查看详情」「加入对比」「加入定投计划」。 | ||
| - 卡片右侧 Mini Chart:7D/30D 走势,使用日线数据。 | ||
|
|
||
| ### 7.4 对比与收藏 | ||
|
|
||
| - 用户可勾选最多 3 支基金进入对比抽屉,展示指标雷达图和关键表格。 | ||
| - 收藏逻辑存储在本地(未登录)或账号(已登录)。 | ||
|
|
||
| ### 7.5 精选组合 | ||
|
|
||
| - 基于推荐算法(见 §8)输出三套组合,显示资产占比、预期波动、历史最大回撤。 | ||
| - CTA 引导至组合策略文章/模拟盘。 | ||
|
|
||
| ### 7.6 教育与风险提示 | ||
|
|
||
| - FAQ 包含:「什么是指数基金」「如何判断适合自己的风险等级」「定投 vs 一次性买入」。 | ||
| - 风险提示固定展示在页面底部:指数基金存在市场风险,过往表现不代表未来等。 | ||
|
|
||
| ## 8. 推荐/评分逻辑 | ||
|
|
||
| - **评分维度**:长期收益(40%)、波动/回撤(25%)、规模与流动性(20%)、费用(10%)、估值合理性(5%)。 | ||
| - **输入数据**:年化收益、最大回撤、日均成交额、管理费率、估值百分位。 | ||
| - **输出**:综合得分 0-100,映射为「入门优选/稳健优选/进取优选」标签。 | ||
| - **组合建议**:使用现代投资组合理论(MPT)或经验权重(保守 70% 宽基债、30% 宽基股;进取 80% 行业/主题 + 20% 宽基)。 | ||
|
|
||
| ## 9. UX / UI 细节 | ||
|
|
||
| - 色彩:Hero 区使用品牌主色,数据卡片背景浅灰,风险等级使用信号色(绿/黄/橙)。 | ||
| |- 表格滚动:列表默认展示 10 条,滚动加载。 | ||
| - Skeleton/loading:使用骨架屏 + 最近一次缓存数据,避免白屏。 | ||
| - 响应式:移动端以纵向卡片排列,筛选器折叠为 Drawer;桌面端使用并排布局。 | ||
|
|
||
| ## 10. 指标与验证 | ||
|
|
||
| - PV/UV:页面独立访客、回访率。 | ||
| - 转化:点击「查看详情」/「加入定投」人数。 | ||
| - 互动:筛选器使用率、对比抽屉打开率、收藏数。 | ||
| - 数据准确度:定时监控接口失败率、数据延迟。 | ||
|
|
||
| ## 11. 非功能要求 | ||
|
|
||
| - 数据接口需具备重试/降级策略:主源(东方财富)失败时切换新浪接口。 | ||
| - 缓存策略:实时行情 60 秒缓存,历史数据 CDN 缓存 24h。 | ||
| - SEO:页面提供中文 meta、OG 信息,结构化 FAQ(JSON-LD)。 | ||
| - 可访问性:颜色对比度符合 WCAG AA,图表提供数值读屏。 | ||
|
|
||
| ## 12. 交付里程碑(建议) | ||
|
|
||
| | 阶段 | 时间 | 主要输出 | | ||
| | -------- | ------- | ------------------------------ | | ||
| | 需求冻结 | T+3 天 | PRD、API 合同、指标口径 | | ||
| | 设计交付 | T+10 天 | 低保真 + 高保真、响应式规格 | | ||
| | 开发联调 | T+20 天 | 前后端开发、数据同步、冒烟测试 | | ||
| | Beta | T+26 天 | 小流量灰度,监控数据准确性 | | ||
| | 全量上线 | T+30 天 | 对外发布,监控指标 | | ||
|
|
||
| ## 13. 风险与对策 | ||
|
|
||
| - **数据接口变动**:AKShare 接口参数或字段变化 → 对接版本监控,配置化字段映射。 | ||
| - **指数/基金映射不全**:补充人工维护的白名单表,并根据用户搜索行为动态扩充。 | ||
| - **用户认知不足**:教育内容区加入短视频/图解链接、常见问题提示。 | ||
| - **性能压力**:列表分页 + 懒加载 mini chart;历史数据预计算。 | ||
|
|
||
| ## 14. Open Points | ||
|
|
||
| 1. 是否需要在页面内直接引导交易?如果需要,需确认合规与合作渠道。 | ||
| 2. 推荐算法是否可以接入已有的智能投顾引擎?抑或仅基于规则。 | ||
| 3. 是否支持用户登录后的个性化提醒(价格提醒、定投日推送)。 | ||
|
|
||
| ## 15. MVP 实施计划 | ||
|
|
||
| 1. **数据与服务** | ||
| - 新增 `/pages/api/finance/index-funds.ts`,封装调用 `stock_zh_index_spot_em`、`stock_zh_index_daily_em` 的逻辑,返回基础字段(名称、代码、最新价、涨跌幅、近 1 年收益、最大回撤占位)。 | ||
| - 设计 `types/finance.ts`(或类似文件)统一前端字段定义,方便后续扩展估值、回测指标。 | ||
| - 加入简单内存缓存(60s)避免接口过度调用。 | ||
| 2. **前端页面结构** | ||
| - 新建 `pages/finance/index.tsx`,沿用全局布局,包含 Hero、筛选器占位、基金列表三大模块。 | ||
| - 创建 `components/finance/FundCard.tsx` 复用卡片 UI,展示名称、指数、最新价、日涨跌、近 1 年收益、风险标签。 | ||
| - 筛选器 MVP 仅支持「类别」和「风险」两个条件,下钻逻辑后续扩展。 | ||
| - 数据请求通过 `SWR` 或自定义 hook(`models` 目录)调用 `/api/finance/index-funds`,支持 loading/skeleton。 | ||
| 3. **交互与样式** | ||
| - 使用 Bootstrap 栅格 + 自定义 SCSS(`styles/finance.module.scss`)完成响应式布局。 | ||
| - 提供空数据与错误兜底提示,保留 CTA 占位。 | ||
| 4. **测试与验收** | ||
| - `pnpm lint` 通过。 | ||
| - 本地 `next dev` 打开 `/finance` 页面验证数据渲染。 | ||
| - 文档更新:在 `README` 或导航处加入入口说明(若需要)。 | ||
TechQuery marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.