show completed trips on profiles
All checks were successful
pedestrian-simulator / build (push) Successful in 59s

This commit is contained in:
2026-01-14 16:55:49 -07:00
parent bf75e10399
commit f0172afb1e
6 changed files with 556 additions and 18 deletions

View File

@@ -107,6 +107,19 @@ func createTables() {
PRIMARY KEY (user_id, date),
FOREIGN KEY (user_id) REFERENCES users(fitbit_user_id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS completed_trips (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(255),
trip_type ENUM('address', 'kml') DEFAULT 'address',
route_name TEXT,
start_address TEXT,
end_address TEXT,
kml_id INT,
distance DOUBLE,
completed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(fitbit_user_id) ON DELETE CASCADE,
FOREIGN KEY (kml_id) REFERENCES kml_metadata(id) ON DELETE SET NULL
)`,
}
for _, query := range queries {

View File

@@ -52,6 +52,24 @@ type KMLMetadata struct {
Votes int `json:"votes"` // Net votes (calculated from voting system)
}
// CompletedTrip Metadata
type CompletedTrip struct {
ID int `json:"id"`
UserID string `json:"user_id"`
TripType string `json:"trip_type"`
RouteName string `json:"route_name"`
StartAddress string `json:"start_address"`
EndAddress string `json:"end_address"`
KmlID *int `json:"kml_id"`
KmlFilename string `json:"kml_filename,omitempty"`
KmlOwnerID string `json:"kml_owner_id,omitempty"`
KmlDisplayName string `json:"kml_display_name,omitempty"`
KmlVotes int `json:"kml_votes,omitempty"`
KmlDescription string `json:"kml_description,omitempty"`
Distance float64 `json:"distance"`
CompletedAt time.Time `json:"completed_at"`
}
// SetVote sets a user's vote for a KML file in the database
func SetVote(kmlID int, userID string, vote int) error {
if vote == 0 {
@@ -651,12 +669,6 @@ func HandleKMLDownload(w http.ResponseWriter, r *http.Request) {
// HandleUserProfile serves public user profile data
func HandleUserProfile(w http.ResponseWriter, r *http.Request) {
// Authentication optional? Yes, profiles should be public.
// But let's check auth just to be safe if we want to restrict it to logged-in users later.
// Current requirement: "implement profile pages... open the KML details overlay... so that you can do it too".
// Implies logged in users mostly, but let's allow it generally if the files are public.
// Get target user ID from URL
targetID := r.URL.Query().Get("user_id")
if targetID == "" {
http.Error(w, "Missing user_id", http.StatusBadRequest)
@@ -673,6 +685,97 @@ func HandleUserProfile(w http.ResponseWriter, r *http.Request) {
return
}
// Fetch completed trips
rows, err := db.Query(`
SELECT ct.id, ct.trip_type, ct.route_name, ct.start_address, ct.end_address, ct.kml_id, ct.distance, ct.completed_at,
m.filename, m.user_id, u.display_name, m.description,
COALESCE((SELECT SUM(vote) FROM kml_votes WHERE kml_id = m.id), 0) as votes
FROM completed_trips ct
LEFT JOIN kml_metadata m ON ct.kml_id = m.id
LEFT JOIN users u ON m.user_id = u.fitbit_user_id
WHERE ct.user_id = ?
ORDER BY ct.completed_at DESC
LIMIT 20
`, targetID)
var completedTrips []CompletedTrip
if err == nil {
defer rows.Close()
for rows.Next() {
var ct CompletedTrip
var kmlID sql.NullInt64
var kmlFilename, kmlOwnerID, kmlDisplayName, kmlDescription sql.NullString
var kmlVotes sql.NullInt64
err := rows.Scan(&ct.ID, &ct.TripType, &ct.RouteName, &ct.StartAddress, &ct.EndAddress, &kmlID, &ct.Distance, &ct.CompletedAt,
&kmlFilename, &kmlOwnerID, &kmlDisplayName, &kmlDescription, &kmlVotes)
if err == nil {
if kmlID.Valid {
id := int(kmlID.Int64)
ct.KmlID = &id
ct.KmlFilename = kmlFilename.String
ct.KmlOwnerID = kmlOwnerID.String
ct.KmlDisplayName = kmlDisplayName.String
ct.KmlDescription = kmlDescription.String
ct.KmlVotes = int(kmlVotes.Int64)
}
completedTrips = append(completedTrips, ct)
}
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
json.NewEncoder(w).Encode(map[string]interface{}{
"user": user,
"completed_trips": completedTrips,
})
}
// HandleTripComplete records a completed trip
func HandleTripComplete(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
userID, ok := getUserID(r.Context())
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
var req struct {
Type string `json:"type"`
RouteName string `json:"route_name"`
StartAddress string `json:"start_address"`
EndAddress string `json:"end_address"`
KmlFilename string `json:"kml_filename"`
KmlOwnerID string `json:"kml_owner_id"`
Distance float64 `json:"distance"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
var kmlID interface{} = nil
if req.Type == "kml" {
var id int
err := db.QueryRow("SELECT id FROM kml_metadata WHERE user_id = ? AND filename = ?", req.KmlOwnerID, req.KmlFilename).Scan(&id)
if err == nil {
kmlID = id
}
}
_, err := db.Exec(`
INSERT INTO completed_trips (user_id, trip_type, route_name, start_address, end_address, kml_id, distance)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, userID, req.Type, req.RouteName, req.StartAddress, req.EndAddress, kmlID, req.Distance)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to save completed trip: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

View File

@@ -140,7 +140,10 @@ func main() {
// 7. User Profile Endpoint
http.HandleFunc("/api/user/profile", RequireAuth(HandleUserProfile))
// 8. Start Server
// 8. Trip Completion Endpoint
http.HandleFunc("/api/trip/complete", RequireAuth(HandleTripComplete))
// 9. Start Server
binding := "0.0.0.0:8080"
fmt.Printf("Server starting on http://%s\n", binding)
log.Fatal(http.ListenAndServe(binding, RecoveryMiddleware(http.DefaultServeMux)))