Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,42 @@
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RailLOOP</title>

<!-- Analytics Anchors -->
<!-- Google Analytics 4 (Handled by react-ga4 in JS, but tag manager can go here) -->
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
</script>

<!-- Baidu Tongji -->
<script>
var _hmt = _hmt || [];
(function() {
var baiduId = "%VITE_BAIDU_TONGJI_ID%"; // Replaced via Vite env or left empty
if(baiduId && !baiduId.startsWith("%")) {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?" + baiduId;
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
}
})();
</script>

<!-- Bing UET -->
<script>
(function(w,d,t,r,u) {
var bingId = "%VITE_BING_UET_ID%";
if(bingId && !bingId.startsWith("%")) {
var f,n,i;
w[u]=w[u]||[],f=function(){var o={ti:bingId, enableAutoSpaTracking: true};o.q=w[u],w[u]=new UET(o),w[u].push("pageLoad")},
n=d.createElement(t),n.src=r,n.async=1,n.onload=n.onreadystatechange=function(){var s=this.readyState;s&&s!=="loaded"&&s!=="complete"||(f(),n.onload=n.onreadystatechange=null)},
i=d.getElementsByTagName(t)[0],i.parentNode.insertBefore(n,i)
}
})(window,document,"script","//bat.bing.com/bat.js","uetq");
</script>

</head>

<body translate="no">
Expand Down
90 changes: 90 additions & 0 deletions scripts/build_seo_data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import fs from 'fs';
import path from 'path';

function buildSeoData() {
const data = {
title: "RailLOOP",
description: "",
features: [],
lines: [],
companies: [],
stations: []
};

// 1. Parse Locales (Primary zh-CN for SEO)
try {
const localePath = path.join(process.cwd(), 'public', 'locales', 'zh-CN', 'translation.json');
if (fs.existsSync(localePath)) {
const zh = JSON.parse(fs.readFileSync(localePath, 'utf8'));
data.description = zh.app?.desc || zh.app?.subtitle || "乗り鉄 / 铁道旅行 / 铁路行程管理与记录工具";

// Extract feature keywords
const keys = ['header', 'tripEdit', 'app', 'pin', 'stats'];
keys.forEach(k => {
if(zh[k]) {
Object.values(zh[k]).forEach(v => {
if (typeof v === 'string' && v.length < 30) data.features.push(v);
});
}
});
}
} catch(e) { console.error(e); }

// 2. Parse Changelog
try {
const changelogPath = path.join(process.cwd(), 'public', 'changelog.json');
if (fs.existsSync(changelogPath)) {
const clog = JSON.parse(fs.readFileSync(changelogPath, 'utf8'));
if (clog.logs) {
clog.logs.forEach(log => {
if (log.content) data.features.push(log.content.substring(0, 50));
if (log.features) {
Object.values(log.features).forEach(f => data.features.push(f));
}
});
}
}
} catch(e) { console.error(e); }

// 3. Parse Company Data
try {
const companyPath = path.join(process.cwd(), 'public', 'company_data.json');
if (fs.existsSync(companyPath)) {
const companies = JSON.parse(fs.readFileSync(companyPath, 'utf8'));
data.companies = Object.keys(companies);
}
} catch(e) { console.error(e); }

// 4. Parse GeoJSON lines and stations (if geojson exists)
try {
const geojsonDir = path.join(process.cwd(), 'public', 'geojson');
if (fs.existsSync(geojsonDir)) {
const files = fs.readdirSync(geojsonDir).filter(f => f.endsWith('.geojson'));
files.forEach(f => {
const geoContent = JSON.parse(fs.readFileSync(path.join(geojsonDir, f), 'utf8'));
if (geoContent.features) {
geoContent.features.forEach(feat => {
if (feat.properties) {
if (feat.properties.type === 'line' && feat.properties.name) {
data.lines.push(feat.properties.name);
}
if (feat.properties.type === 'station' && feat.properties.name) {
data.stations.push(feat.properties.name);
}
}
});
}
});
}
} catch(e) { console.error(e); }

// Deduplicate and trim
data.features = [...new Set(data.features)].filter(Boolean);
data.lines = [...new Set(data.lines)].filter(Boolean);
data.stations = [...new Set(data.stations)].filter(Boolean);

fs.writeFileSync(path.join(process.cwd(), 'public', 'seo_data.json'), JSON.stringify(data, null, 2));
console.log("Generated public/seo_data.json successfully.");
}

buildSeoData();
94 changes: 94 additions & 0 deletions scripts/submit_search_engines.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import https from 'https';
import fs from 'fs';
import path from 'path';

const DOMAIN = "https://rail.s3xyseia.xyz";
const SITEMAP_URL = `${DOMAIN}/sitemap.xml`;

// API Keys from environment
const BAIDU_TOKEN = process.env.BAIDU_TOKEN;
const BING_API_KEY = process.env.BING_API_KEY;

console.log("Starting Search Engine Sitemap Submission...");

// 1. Google (Ping)
const pingGoogle = () => {
https.get(`https://www.google.com/ping?sitemap=${encodeURIComponent(SITEMAP_URL)}`, (res) => {
console.log(`[Google Ping] Status: ${res.statusCode}`);
}).on('error', (e) => {
console.error(`[Google Ping] Error: ${e.message}`);
});
};

// 2. Bing (Ping & Webmaster API)
const submitBing = () => {
// Standard Ping
https.get(`https://www.bing.com/ping?sitemap=${encodeURIComponent(SITEMAP_URL)}`, (res) => {
console.log(`[Bing Ping] Status: ${res.statusCode}`);
}).on('error', (e) => {
console.error(`[Bing Ping] Error: ${e.message}`);
});

// Webmaster API
if (BING_API_KEY) {
const data = JSON.stringify({
"siteUrl": DOMAIN,
"urlList": [DOMAIN]
});

const options = {
hostname: 'ssl.bing.com',
path: `/webmaster/api.svc/json/SubmitUrlbatch?apikey=${BING_API_KEY}`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': data.length
}
};

const req = https.request(options, (res) => {
let body = '';
res.on('data', d => body += d);
res.on('end', () => console.log(`[Bing Webmaster API] Status: ${res.statusCode}, Response: ${body}`));
});
req.on('error', e => console.error(`[Bing Webmaster API] Error: ${e.message}`));
req.write(data);
req.end();
} else {
console.log("[Bing Webmaster API] Skipped: BING_API_KEY not set.");
}
};

// 3. Baidu (Webmaster API)
const submitBaidu = () => {
if (BAIDU_TOKEN) {
const data = `${DOMAIN}/`;

// Ensure hostname has no protocol
const host = DOMAIN.replace(/^https?:\/\//, '');
const options = {
hostname: 'data.zz.baidu.com',
path: `/urls?site=${encodeURIComponent(DOMAIN)}&token=${BAIDU_TOKEN}`,
method: 'POST',
headers: {
'Content-Type': 'text/plain',
'Content-Length': data.length
}
};

const req = https.request(options, (res) => {
let body = '';
res.on('data', d => body += d);
res.on('end', () => console.log(`[Baidu API] Status: ${res.statusCode}, Response: ${body}`));
});
req.on('error', e => console.error(`[Baidu API] Error: ${e.message}`));
req.write(data);
req.end();
} else {
console.log("[Baidu API] Skipped: BAIDU_TOKEN not set.");
}
};

pingGoogle();
submitBing();
submitBaidu();
4 changes: 4 additions & 0 deletions src/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { FabButton } from './components/map/FabButton';
import { LocateButton } from './components/map/LocateButton';
import { Header } from './components/layout/Header';
import { BottomNav } from './components/layout/BottomNav';
import { trackEvent, AnalyticsEvents } from './utils/analytics';
import { TripsPage } from './pages/TripsPage';
import { StatsPage } from './pages/StatsPage';
import { useStore } from './store';
Expand Down Expand Up @@ -933,6 +934,7 @@ export const AppLayout: React.FC = () => {

// --- 4. File Handlers ---
const handleExportKML = async () => {
trackEvent({ category: AnalyticsEvents.USER_ACTION, action: 'Export_KML' });
if (isExportingKML) return;
setIsExportingKML(true);
setTimeout(async () => {
Expand Down Expand Up @@ -977,6 +979,7 @@ export const AppLayout: React.FC = () => {
};

const handleExportUserData = () => {
trackEvent({ category: AnalyticsEvents.USER_ACTION, action: 'Export_JSON_Backup' });
const linesUsed = new Set();
const companiesUsed = new Set();
trips.forEach(trip => { (trip.segments || []).forEach((s: any) => { if (s.lineKey) { linesUsed.add(s.lineKey); const meta = railwayData[s.lineKey]?.meta; if (meta && meta.company) companiesUsed.add(meta.company); } }); });
Expand All @@ -987,6 +990,7 @@ export const AppLayout: React.FC = () => {
};

const handleImportUserData = (event: any) => {
trackEvent({ category: AnalyticsEvents.USER_ACTION, action: 'Import_JSON_Backup' });
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
Expand Down
3 changes: 2 additions & 1 deletion src/components/LoginModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ const renderMarkdown = (text) => {
return elements;
};

import { trackEvent, AnalyticsEvents } from "../utils/analytics";
export const LoginModal = ({ isOpen, onClose, onLoginSuccess, user }) => {
const [isRegistering, setIsRegistering] = useState(false);
const [username, setUsername] = useState('');
Expand Down Expand Up @@ -245,7 +246,7 @@ export const LoginModal = ({ isOpen, onClose, onLoginSuccess, user }) => {
} else {
result = await api.login(username, password);
}
onLoginSuccess(result);
trackEvent({category: AnalyticsEvents.USER_ACTION, action: "Login_Success"}); onLoginSuccess(result);
onClose();
} catch (err) {
setError(err.message);
Expand Down
2 changes: 2 additions & 0 deletions src/components/map/PinEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useShallow } from 'zustand/react/shallow';
import { useUserData } from '../../hooks/useUserData';
import { useTranslation } from 'react-i18next';
import { showConfirm } from '../../utils/alerts';
import { trackEvent, AnalyticsEvents } from '../../utils/analytics';

const COLOR_PALETTE = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#3b82f6', '#a855f7', '#ec4899', '#64748b'];

Expand All @@ -26,6 +27,7 @@ export const PinEditor: React.FC = () => {

const savePin = () => {
if (!editingPin) return;
trackEvent({ category: AnalyticsEvents.USER_ACTION, action: editingPin.isTemp ? 'Add_Pin' : 'Edit_Pin' });
const newPin = { ...editingPin, id: editingPin.isTemp ? Date.now() : editingPin.id };
delete newPin.isTemp;

Expand Down
3 changes: 3 additions & 0 deletions src/components/modals/AddToFolderModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useUserData } from '../../hooks/useUserData';
import { calculateLatestStats } from '../../core/tripCalculator';
import { useTranslation } from 'react-i18next';
import { showAlert } from '../../utils/alerts';
import { trackEvent, AnalyticsEvents } from '../../utils/analytics';

export const AddToFolderModal: React.FC = () => {
const { isOpen, trip, folders, user, trips, pins, badgeSettings, segmentGeometries, railwayData, geoData } = useStore(useShallow(state => ({
Expand Down Expand Up @@ -44,8 +45,10 @@ export const AddToFolderModal: React.FC = () => {
const currentIds = new Set(f.trip_ids || []);
if (currentIds.has(trip.id)) {
currentIds.delete(trip.id);
trackEvent({ category: AnalyticsEvents.USER_ACTION, action: 'Remove_From_Folder' });
} else {
currentIds.add(trip.id);
trackEvent({ category: AnalyticsEvents.USER_ACTION, action: 'Add_To_Folder' });
}
return { ...f, trip_ids: Array.from(currentIds) };
}
Expand Down
1 change: 1 addition & 0 deletions src/components/modals/GithubRegisterModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useShallow } from 'zustand/react/shallow';
import { useUserData } from '../../hooks/useUserData';
import { useTranslation } from 'react-i18next';

import { trackEvent, AnalyticsEvents } from "../utils/analytics";
export const GithubRegisterModal: React.FC = () => {
const { isOpen, regToken } = useStore(useShallow(state => ({
isOpen: state.modals.isGithubRegOpen,
Expand Down
3 changes: 3 additions & 0 deletions src/components/modals/TripEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useShallow } from 'zustand/react/shallow';
import { useUserData } from '../../hooks/useUserData';
import { useTranslation } from 'react-i18next';
import { showAlert, showConfirm } from '../../utils/alerts';
import { trackEvent, AnalyticsEvents } from '../../utils/analytics';

export const TripEditor: React.FC = () => {
const {
Expand Down Expand Up @@ -47,6 +48,7 @@ export const TripEditor: React.FC = () => {
const [allowedLines, setAllowedLines] = useState<string[] | null>(null);

const onSave = async () => {
trackEvent({ category: AnalyticsEvents.TRIP_ACTION, action: isEditing ? 'Edit_Trip' : 'Add_Trip' });
// Validation logic
const validSegments = form.segments?.filter(s => s.fromId !== s.toId) || [];
if (validSegments.length === 0) { showAlert(t("tripEdit.atLeastOne", "至少包含一段有效行程")); return; }
Expand Down Expand Up @@ -148,6 +150,7 @@ export const TripEditor: React.FC = () => {
}, [autoRouteEasterEggType, isOpen, autoForm, form.date]);

const onAutoSearch = (retryWithInfiniteSearch = false) => {
trackEvent({ category: AnalyticsEvents.TRIP_ACTION, action: 'Auto_Route_Search' });
const isInfinite = retryWithInfiniteSearch === true;
const { startLine, startStation, endLine, endStation } = autoForm;
if (!startLine || !startStation || !endLine || !endStation) return;
Expand Down
Loading