Skip to content

front end draft for Daniel to use #1

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
merged 2 commits into from
Apr 14, 2025
Merged
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
.env
.specstory/
.DS_Store
playwright-report/
.vscode/
37 changes: 36 additions & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
@@ -21,7 +21,28 @@

app.json = CustomJsonProvider(app)

CORS(app)
# Configure CORS to handle preflight requests
# TODO Daniel not sure what I should have been doing so have just bunged this in for now
CORS(
app,
resources={r"/*": {
"origins": ["http://127.0.0.1:5500", "http://localhost:5500"],
"supports_credentials": True,
"allow_headers": ["Content-Type", "Authorization"],
"methods": ["GET", "POST", "OPTIONS"]
}}
)

# Add a route to handle OPTIONS requests explicitly
@app.route('/', defaults={'path': ''}, methods=['OPTIONS'])
@app.route('/<path:path>', methods=['OPTIONS'])
def handle_options(path):
response = make_response()
response.headers.add('Access-Control-Allow-Origin', request.headers.get('Origin', '*'))
response.headers.add('Access-Control-Allow-Headers', 'Content-Type, Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
response.headers.add('Access-Control-Allow-Credentials', 'true')
return response

MINIMUM_PASSWORD_LENGTH = 5

@@ -80,6 +101,13 @@ def register():
@jwt_required()
def self_profile():
username = get_current_user().username

# Check if the user exists
if username not in users:
return make_response(jsonify({
"success": False,
"reason": "User not found"
}), 404)

return jsonify(
{
@@ -93,6 +121,13 @@ def self_profile():
@app.route("/profile/<profile_username>")
@jwt_required(optional=True)
def other_profile(profile_username):
# Check if the user exists
if profile_username not in users:
return make_response(jsonify({
"success": False,
"reason": f"User {profile_username} not found"
}), 404)

current_user = get_current_user()

followers = follows.get_inverse(profile_username)
32 changes: 32 additions & 0 deletions front-end/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Purple Forest front end

Native web components. Vanilla JS. Vanilla CSS. Playwright tests.

## Running the Application

1. Clone the repository
2. Open `index.html` in your browser - make sure the backend is running
3. Login with the demo account 'sample' password 'sosecret'

This front end doesn't have a build step. It's just a collection of web components.

## Event driven architecture

Here's how events flow in this application:

### User Actions → Component Events

1. User clicks "Login" → LoginComponent handles form submission
1. Component calls apiService.login()
1. On success, component dispatches "auth-change" event

### State Changes → Component Updates

1. "auth-change" event → index.mjs updates application state
1. State update triggers "state-change" event
1. Components listen for "state-change" and update their display

### Component Communication

1. Components dispatch custom events (e.g., "show-view") for view changes that aren't related to auth
1. Other components listen for these events and respond accordingly
241 changes: 241 additions & 0 deletions front-end/api/apiService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
// api/apiService.js
class ApiService {
constructor(baseUrl = "http://127.0.0.1:3000") {
this.baseUrl = baseUrl;
this.token = null;
this.currentUser = null;
}

// ==== AUTH METHODS ====
async login(username, password) {
try {
const response = await this._post("/login", {username, password});

// Backend returns { success: true, token: "..." }
if (response.success && response.token) {
this.token = response.token;

// Create a user object with the username
const userData = {
username: username,
token: response.token,
};

return {
success: true,
access_token: response.token,
user: userData,
};
}

return response;
} catch (error) {
return {
success: false,
detail: error.message,
};
}
}

async signup(username, password, displayName) {
try {
// Use /register instead of /signup
const response = await this._post("/register", {
username,
password,
// Note: backend doesn't use displayName
});

if (response.success && response.token) {
this.token = response.token;

// Create user object
const userData = {
username: username,
token: response.token,
displayName: displayName || username,
};

return {
success: true,
access_token: response.token,
user: userData,
};
}

return response;
} catch (error) {
return {
success: false,
detail: error.message,
};
}
}

async logout() {
this.token = null;
this.currentUser = null;
return {success: true};
}

// ==== TWEET METHODS ====
async getFeed() {
return this._get("/blooms/feed");
}

async getBlooms() {
return this._get("/blooms");
}

async postBloom(content) {
return this._post("/bloom", {content});
}

async likeBloom(bloomId) {
return this._post(`/blooms/${bloomId}/like`);
}

// ==== USER METHODS ====
async getProfile() {
console.log("[ApiService] Getting profile");
try {
const response = await this._get("/profile");
console.log("[ApiService] Profile response:", response);
return response;
} catch (error) {
console.error("[ApiService] Profile error:", error);
// Return null instead of throwing to prevent component errors
return null;
}
}

async getUserProfile(username) {
// If no username is provided, return null to prevent API errors
if (!username || username.trim() === "") {
console.error(
"[ApiService] Invalid or empty username provided for profile request"
);
return null;
}

console.log(`[ApiService] Getting user profile: ${username}`);

try {
const response = await this._get(`/profile/${username}`);
console.log("[ApiService] User profile response:", response);
return response;
} catch (error) {
console.error("[ApiService] User profile error:", error);
// Return null instead of throwing to prevent component errors
return null;
}
}

async followUser(username) {
return this._post(`/users/${username}/follow`);
}

// ==== HELPER METHODS ====
async _get(endpoint) {
try {
console.log(`[ApiService] GET request to: ${this.baseUrl}${endpoint}`);
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: "GET",
headers: this._getHeaders(),
mode: "cors",
// No credentials option - we're using token auth in headers
});
return this._handleResponse(response);
} catch (error) {
console.error(`[ApiService] GET error for ${endpoint}:`, error);
return null;
}
}

async _post(endpoint, data) {
try {
console.log(
`[ApiService] POST request to: ${this.baseUrl}${endpoint}`,
data
);
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: "POST",
headers: this._getHeaders(),
body: JSON.stringify(data),
mode: "cors",
// No credentials option - we're using token auth in headers
});
return this._handleResponse(response);
} catch (error) {
console.error(`[ApiService] POST error for ${endpoint}:`, error);
return {
success: false,
detail: error.message,
};
}
}

_getHeaders() {
const headers = {
"Content-Type": "application/json",
Accept: "application/json",
};
if (this.token) {
// Ensure the token is properly formatted for JWT
headers["Authorization"] = `Bearer ${this.token}`;
}
return headers;
}

async _handleResponse(response) {
try {
// Log response status and headers for debugging
console.log(`[ApiService] Response status: ${response.status}`);

if (!response.ok) {
console.error(
`[ApiService] Error response: ${response.status} ${response.statusText}`
);

// Try to parse the error response as JSON
try {
const errorData = await response.json();
return {
success: false,
detail:
errorData.reason ||
`API error: ${response.status} ${response.statusText}`,
status: response.status,
};
} catch (parseError) {
return {
success: false,
detail: `API error: ${response.status} ${response.statusText}`,
status: response.status,
};
}
}

// Check if response is JSON
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
const data = await response.json();
return data;
} else {
console.warn("[ApiService] Response is not JSON");
return {
success: true,
message: "Operation completed but response was not JSON",
};
}
} catch (error) {
console.error("[ApiService] Error handling response:", error);
return {
success: false,
detail: error.message || "Failed to process response",
};
}
}
}

export const apiService = new ApiService();
Loading
Oops, something went wrong.