show completed trips on profiles
All checks were successful
pedestrian-simulator / build (push) Successful in 59s
All checks were successful
pedestrian-simulator / build (push) Successful in 59s
This commit is contained in:
13
server/db.go
13
server/db.go
@@ -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 {
|
||||
|
||||
117
server/kml.go
117
server/kml.go
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
|
||||
Reference in New Issue
Block a user